diff --git a/commands/cluster_command_launcher.go b/commands/cluster_command_launcher.go index a68f429..101cd76 100644 --- a/commands/cluster_command_launcher.go +++ b/commands/cluster_command_launcher.go @@ -31,7 +31,7 @@ import ( const defaultLogPath = "/opt/vertica/log/vcluster.log" const defaultExecutablePath = "/opt/vertica/bin/vcluster" -const CLIVersion = "1.2.0" +const CLIVersion = "2.0.0" const vclusterLogPathEnv = "VCLUSTER_LOG_PATH" const vclusterKeyFileEnv = "VCLUSTER_KEY_FILE" const vclusterCertFileEnv = "VCLUSTER_CERT_FILE" @@ -152,16 +152,16 @@ const ( configShowSubCmd = "show" replicationSubCmd = "replication" startReplicationSubCmd = "start" - listAllNodesSubCmd = "list_allnodes" + listAllNodesSubCmd = "list_all_nodes" startDBSubCmd = "start_db" dropDBSubCmd = "drop_db" - addSCSubCmd = "db_add_subcluster" - removeSCSubCmd = "db_remove_subcluster" + addSCSubCmd = "add_subcluster" + removeSCSubCmd = "remove_subcluster" stopSCSubCmd = "stop_subcluster" - addNodeSubCmd = "db_add_node" + addNodeSubCmd = "add_node" startSCSubCmd = "start_subcluster" stopNodeCmd = "stop_node" - removeNodeSubCmd = "db_remove_node" + removeNodeSubCmd = "remove_node" restartNodeSubCmd = "restart_node" reIPSubCmd = "re_ip" sandboxSubCmd = "sandbox_subcluster" diff --git a/commands/cmd_add_node.go b/commands/cmd_add_node.go index 6a3ac04..8c3bfb3 100644 --- a/commands/cmd_add_node.go +++ b/commands/cmd_add_node.go @@ -63,11 +63,11 @@ Omitting the option will skip this node trimming process. Examples: # Add a single host to the existing database with config file - vcluster db_add_node --db-name test_db --new-hosts 10.20.30.43 \ + vcluster add_node --db-name test_db --new-hosts 10.20.30.43 \ --config /opt/vertica/config/vertica_cluster.yaml # Add multiple hosts to the existing database with user input - vcluster db_add_node --db-name test_db --new-hosts 10.20.30.43,10.20.30.44 \ + vcluster add_node --db-name test_db --new-hosts 10.20.30.43,10.20.30.44 \ --data-path /data --hosts 10.20.30.40 \ --node-names v_test_db_node0001,v_test_db_node0002 `, diff --git a/commands/cmd_add_subcluster.go b/commands/cmd_add_subcluster.go index 51c1eef..e86dce0 100644 --- a/commands/cmd_add_subcluster.go +++ b/commands/cmd_add_subcluster.go @@ -58,22 +58,22 @@ the --is-primary option. Examples: # Add a subcluster with config file - vcluster db_add_subcluster --subcluster sc1 \ + vcluster add_subcluster --subcluster sc1 \ --config /opt/vertica/config/vertica_cluster.yaml \ --is-primary --control-set-size 1 # Add a subcluster with user input - vcluster db_add_subcluster --subcluster sc1 --db-name test_db \ + vcluster add_subcluster --subcluster sc1 --db-name test_db \ --hosts 10.20.30.40,10.20.30.41,10.20.30.42 \ --is-primary --control-set-size -1 # Add a subcluster and new nodes in the subcluster with config file - vcluster db_add_subcluster --subcluster sc1 \ + vcluster add_subcluster --subcluster sc1 \ --config /opt/vertica/config/vertica_cluster.yaml \ --is-primary --control-set-size 1 --new-hosts 10.20.30.43 # Add a subcluster new nodes in the subcluster with user input - vcluster db_add_subcluster --subcluster sc1 --db-name test_db \ + vcluster add_subcluster --subcluster sc1 --db-name test_db \ --hosts 10.20.30.40,10.20.30.41,10.20.30.42 \ --is-primary --control-set-size -1 --new-hosts 10.20.30.43 `, @@ -208,15 +208,17 @@ func (c *CmdAddSubcluster) Run(vcc vclusterops.ClusterCommands) error { } if len(options.NewHosts) > 0 { - fmt.Printf("Adding hosts %v to subcluster %s\n", - options.NewHosts, options.SCName) + vlog.DisplayColorInfo("Adding hosts %v to subcluster %s", options.NewHosts, options.SCName) options.VAddNodeOptions.DatabaseOptions = c.addSubclusterOptions.DatabaseOptions options.VAddNodeOptions.SCName = c.addSubclusterOptions.SCName vdb, err := vcc.VAddNode(&options.VAddNodeOptions) if err != nil { - vcc.LogError(err, "failed to add nodes into the new subcluster") + const msg = "Failed to add nodes into the new subcluster" + vcc.LogError(err, msg) + fmt.Printf("%s\nHint: subcluster %q is successfully created, you should use add_node to add nodes\n", + msg, options.VAddNodeOptions.SCName) return err } // update db info in the config file diff --git a/commands/cmd_base.go b/commands/cmd_base.go index aadde2e..4032908 100644 --- a/commands/cmd_base.go +++ b/commands/cmd_base.go @@ -39,7 +39,7 @@ type CmdBase struct { argv []string parser *pflag.FlagSet - // for some commands like list_allnodes, we want to allow the output to be written + // for some commands like list_all_nodes, we want to allow the output to be written // to a file instead of being displayed in stdout. This is the file the output will // be written to output string diff --git a/commands/cmd_config_recover.go b/commands/cmd_config_recover.go index 173b2f4..c24e3be 100644 --- a/commands/cmd_config_recover.go +++ b/commands/cmd_config_recover.go @@ -47,6 +47,9 @@ func makeCmdConfigRecover() *cobra.Command { `This subcommand is used to recover the content of the config file. You must provide all the hosts that participate in the database. +db-name and password are required fields in case of running db. In case the password +is wrong or not provided, config info is recovered from the catalog editor. +For accurate sandbox information recovery, the database needs to be running. If there is an existing file at the provided config file location, the recover function will not create a new config file unless you explicitly specify --overwrite. @@ -55,15 +58,15 @@ Examples: # Recover the config file to the default location vcluster manage_config recover --db-name test_db \ --hosts 10.20.30.41,10.20.30.42,10.20.30.43 \ - --catalog-path /data --depot-path /data + --catalog-path /data --depot-path /data --password "" # Recover the config file to /tmp/vertica_cluster.yaml vcluster manage_config recover --db-name test_db \ --hosts 10.20.30.41,10.20.30.42,10.20.30.43 \ --catalog-path /data --depot-path /data \ - --config /tmp/vertica_cluster.yaml + --config /tmp/vertica_cluster.yaml --password "" `, - []string{dbNameFlag, hostsFlag, catalogPathFlag, depotPathFlag, ipv6Flag, configFlag}, + []string{dbNameFlag, hostsFlag, catalogPathFlag, depotPathFlag, ipv6Flag, configFlag, passwordFlag}, ) // require db-name, hosts, catalog-path, and data-path @@ -83,6 +86,12 @@ func (c *CmdConfigRecover) setLocalFlags(cmd *cobra.Command) { false, "overwrite the existing config file", ) + cmd.Flags().BoolVar( + &c.recoverConfigOptions.AfterRevive, + "after-revive", + false, + "whether recover config file right after reviving a database", + ) } func (c *CmdConfigRecover) Parse(inputArgv []string, logger vlog.Printer) error { @@ -100,7 +109,7 @@ func (c *CmdConfigRecover) validateParse(logger vlog.Printer) error { return err } - return nil + return c.setDBPassword(&c.recoverConfigOptions.DatabaseOptions) } func (c *CmdConfigRecover) Run(vcc vclusterops.ClusterCommands) error { @@ -110,6 +119,7 @@ func (c *CmdConfigRecover) Run(vcc vclusterops.ClusterCommands) error { return err } // write db info to vcluster config file + vdb.FirstStartAfterRevive = c.recoverConfigOptions.AfterRevive err = writeConfig(&vdb) if err != nil { return fmt.Errorf("fail to write config file, details: %s", err) diff --git a/commands/cmd_create_db.go b/commands/cmd_create_db.go index 7b0e05b..c9e3dcd 100644 --- a/commands/cmd_create_db.go +++ b/commands/cmd_create_db.go @@ -16,6 +16,8 @@ package commands import ( + "fmt" + "github.com/spf13/cobra" "github.com/vertica/vcluster/vclusterops" "github.com/vertica/vcluster/vclusterops/util" @@ -268,7 +270,7 @@ func (c *CmdCreateDB) Run(vcc vclusterops.ClusterCommands) error { // write db info to vcluster config file err := writeConfig(&vdb) if err != nil { - vcc.PrintWarning("fail to write config file, details: %s", err) + fmt.Printf("Warning: Fail to write config file, details: %s\n", err) } vcc.PrintInfo("Created a database with name [%s]", vdb.Name) return nil diff --git a/commands/cmd_list_all_nodes.go b/commands/cmd_list_all_nodes.go index d8f9408..d28c0b4 100644 --- a/commands/cmd_list_all_nodes.go +++ b/commands/cmd_list_all_nodes.go @@ -50,7 +50,7 @@ whether they are up or down. To provide its status, each host must run the spread daemon. You must provide the --hosts option one or more hosts as a comma-separated -list. list_allnodes returns the first response it receives from any host. +list. list_all_nodes returns the first response it receives from any host. The --db-name and --catalog-path options are required only when vcluster cannot obtain node information from a running database and the config file is not @@ -59,7 +59,7 @@ provided. Examples: # List the status of nodes with config file where password authentication is # used to access the database - vcluster list_allnodes --password testpassword \ + vcluster list_all_nodes --password testpassword \ --config /opt/vertica/config/vertica_cluster.yaml `, []string{dbNameFlag, hostsFlag, passwordFlag, ipv6Flag, catalogPathFlag, configFlag, outputFileFlag}, diff --git a/commands/cmd_re_ip.go b/commands/cmd_re_ip.go index 1b14ad8..0605ada 100644 --- a/commands/cmd_re_ip.go +++ b/commands/cmd_re_ip.go @@ -49,7 +49,7 @@ The database must be down to change the IP addresses with re_ip. If the database is up, you must run restart_node after re_ip for the IP changes to take effect. -The file specified by the re_ip-file option must be a JSON file in the +The file specified by the re-ip-file option must be a JSON file in the following format: [ {"from_address": "10.20.30.40", "to_address": "10.20.30.41"}, diff --git a/commands/cmd_remove_node.go b/commands/cmd_remove_node.go index 58acbd7..fc8640b 100644 --- a/commands/cmd_remove_node.go +++ b/commands/cmd_remove_node.go @@ -53,12 +53,12 @@ You cannot remove nodes from a sandboxed subcluster in an Eon Mode database. Examples: # Remove multiple nodes from the existing database with config file - vcluster db_remove_node --db-name test_db \ + vcluster remove_node --db-name test_db \ --remove 10.20.30.40,10.20.30.42 \ --config /opt/vertica/config/vertica_cluster.yaml # Remove a single node from the existing database with user input - vcluster db_remove_node --db-name test_db --remove 10.20.30.42 \ + vcluster remove_node --db-name test_db --remove 10.20.30.42 \ --hosts 10.20.30.40 --data-path /data `, []string{dbNameFlag, configFlag, hostsFlag, ipv6Flag, catalogPathFlag, dataPathFlag, depotPathFlag, passwordFlag}, diff --git a/commands/cmd_remove_subcluster.go b/commands/cmd_remove_subcluster.go index ad2d820..ee1e8e3 100644 --- a/commands/cmd_remove_subcluster.go +++ b/commands/cmd_remove_subcluster.go @@ -51,11 +51,11 @@ subcluster. Examples: # Remove a subcluster with config file - vcluster db_remove_subcluster --subcluster sc1 \ + vcluster remove_subcluster --subcluster sc1 \ --config /opt/vertica/config/vertica_cluster.yaml # Remove a subcluster with user input - vcluster db_remove_subcluster --db-name test_db \ + vcluster remove_subcluster --db-name test_db \ --hosts 10.20.30.40,10.20.30.41,10.20.30.42 --subcluster sc1 \ --data-path /data --depot-path /data `, @@ -77,7 +77,7 @@ Examples: // setLocalFlags will set the local flags the command has func (c *CmdRemoveSubcluster) setLocalFlags(cmd *cobra.Command) { cmd.Flags().StringVar( - &c.removeScOptions.SubclusterToRemove, + &c.removeScOptions.SCName, subclusterFlag, "", "Name of subcluster to be removed", @@ -139,7 +139,7 @@ func (c *CmdRemoveSubcluster) Run(vcc vclusterops.ClusterCommands) error { vcc.PrintWarning("fail to write config file, details: %s", err) } vcc.PrintInfo("Successfully removed subcluster %s from database %s", - options.SubclusterToRemove, options.DBName) + options.SCName, options.DBName) return nil } diff --git a/commands/cmd_revive_db.go b/commands/cmd_revive_db.go index 260c428..631c700 100644 --- a/commands/cmd_revive_db.go +++ b/commands/cmd_revive_db.go @@ -180,6 +180,7 @@ func (c *CmdReviveDB) Run(vcc vclusterops.ClusterCommands) error { } // write db info to vcluster config file + vdb.FirstStartAfterRevive = true err = writeConfig(vdb) if err != nil { vcc.PrintWarning("fail to write config file, details: %s", err) diff --git a/commands/cmd_scrutinize.go b/commands/cmd_scrutinize.go index 6a7b2d8..8bae24a 100644 --- a/commands/cmd_scrutinize.go +++ b/commands/cmd_scrutinize.go @@ -185,6 +185,12 @@ func (c *CmdScrutinize) setLocalFlags(cmd *cobra.Command) { "Include information describing all UDX functions, "+ "which can be expensive to gather on Eon", ) + cmd.Flags().BoolVar( + &c.sOptions.SkipCollectLibs, + "skip-collect-libraries", + false, + "Skip gathering linked and catalog shared libraries", + ) } func (c *CmdScrutinize) Parse(inputArgv []string, logger vlog.Printer) error { diff --git a/commands/cmd_start_db.go b/commands/cmd_start_db.go index 2bc3ea3..f14ab21 100644 --- a/commands/cmd_start_db.go +++ b/commands/cmd_start_db.go @@ -30,9 +30,9 @@ type CmdStartDB struct { CmdBase startDBOptions *vclusterops.VStartDatabaseOptions - Force bool // force cleanup to start the database + Force bool // Force cleanup to start the database AllowFallbackKeygen bool // Generate spread encryption key from Vertica. Use under support guidance only - IgnoreClusterLease bool // ignore the cluster lease in communal storage + IgnoreClusterLease bool // Ignore the cluster lease in communal storage Unsafe bool // Start database unsafely, skipping recovery. Fast bool // Attempt fast startup database } @@ -88,6 +88,8 @@ func (c *CmdStartDB) setLocalFlags(cmd *cobra.Command) { util.DefaultTimeoutSeconds, "The timeout (in seconds) to wait for polling node state operation", ) + // Update description of hosts flag locally for a detailed hint + cmd.Flags().Lookup(hostsFlag).Usage = "Comma-separated list of hosts in database. This is used to start sandboxed hosts" } // setHiddenFlags will set the hidden flags the command has. @@ -165,6 +167,12 @@ func (c *CmdStartDB) Run(vcc vclusterops.ClusterCommands) error { vcc.V(1).Info("Called method Run()") options := c.startDBOptions + dbConfig, readConfigErr := readConfig() + if readConfigErr == nil { + options.FirstStartAfterRevive = dbConfig.FirstStartAfterRevive + } else { + vcc.PrintWarning("fail to read config file", "error", readConfigErr) + } vdb, err := vcc.VStartDatabase(options) if err != nil { @@ -175,8 +183,9 @@ func (c *CmdStartDB) Run(vcc vclusterops.ClusterCommands) error { vcc.PrintInfo("Successfully start the database %s", options.DBName) // for Eon database, update config file to fill nodes' subcluster information - if options.IsEon { + if readConfigErr == nil && options.IsEon { // write db info to vcluster config file + vdb.FirstStartAfterRevive = false err := writeConfig(vdb) if err != nil { vcc.PrintWarning("fail to update config file, details: %s", err) diff --git a/commands/cmd_start_replication.go b/commands/cmd_start_replication.go index cc1a89a..5cf63d0 100644 --- a/commands/cmd_start_replication.go +++ b/commands/cmd_start_replication.go @@ -112,7 +112,7 @@ func (c *CmdStartReplication) setLocalFlags(cmd *cobra.Command) { "The target database that we will replicate to", ) cmd.Flags().StringVar( - &c.startRepOptions.Sandbox, + &c.startRepOptions.SandboxName, sandboxFlag, "", "The source sandbox that we will replicate from", diff --git a/commands/cmd_start_subcluster.go b/commands/cmd_start_subcluster.go index 96139e7..514ab5c 100644 --- a/commands/cmd_start_subcluster.go +++ b/commands/cmd_start_subcluster.go @@ -74,7 +74,7 @@ Examples: // setLocalFlags will set the local flags the command has func (c *CmdStartSubcluster) setLocalFlags(cmd *cobra.Command) { cmd.Flags().StringVar( - &c.startScOptions.SubclusterToStart, + &c.startScOptions.SCName, subclusterFlag, "", "Name of subcluster to start", @@ -131,7 +131,7 @@ func (c *CmdStartSubcluster) Run(vcc vclusterops.ClusterCommands) error { } vcc.PrintInfo("Successfully started subcluster %s for database %s", - options.SubclusterToStart, options.DBName) + options.SCName, options.DBName) return nil } diff --git a/commands/cmd_stop_db.go b/commands/cmd_stop_db.go index 196c387..28a03a2 100644 --- a/commands/cmd_stop_db.go +++ b/commands/cmd_stop_db.go @@ -80,7 +80,7 @@ func (c *CmdStopDB) setLocalFlags(cmd *cobra.Command) { " Set this to 0 for Eon database, if you want to forcibly stop the database."), ) cmd.Flags().StringVar( - &c.stopDBOptions.Sandbox, + &c.stopDBOptions.SandboxName, sandboxFlag, "", "Name of the sandbox to stop", @@ -152,8 +152,8 @@ func (c *CmdStopDB) Run(vcc vclusterops.ClusterCommands) error { return err } msg := fmt.Sprintf("Stopped a database with name %s", options.DBName) - if options.Sandbox != "" { - sandboxMsg := fmt.Sprintf(" on sandbox %s", options.Sandbox) + if options.SandboxName != "" { + sandboxMsg := fmt.Sprintf(" on sandbox %s", options.SandboxName) vcc.PrintInfo(msg + sandboxMsg) return nil } diff --git a/commands/vcluster_config.go b/commands/vcluster_config.go index 09dac52..eaf3273 100644 --- a/commands/vcluster_config.go +++ b/commands/vcluster_config.go @@ -49,6 +49,7 @@ type DatabaseConfig struct { IsEon bool `yaml:"eonMode" mapstructure:"eonMode"` CommunalStorageLocation string `yaml:"communalStorageLocation" mapstructure:"communalStorageLocation"` Ipv6 bool `yaml:"ipv6" mapstructure:"ipv6"` + FirstStartAfterRevive bool `yaml:"firstStartAfterRevive" mapstructure:"firstStartAfterRevive"` } // NodeConfig contains node information in the database @@ -232,21 +233,20 @@ func readVDBToDBConfig(vdb *vclusterops.VCoordinationDatabase) (DatabaseConfig, nodeConfig.Subcluster = vnode.Subcluster nodeConfig.Sandbox = vnode.Sandbox - // VER-91869 will replace the path prefixes with full paths if vdb.CatalogPrefix == "" { - nodeConfig.CatalogPath = util.GetPathPrefix(vnode.CatalogPath) + nodeConfig.CatalogPath = vnode.CatalogPath } else { - nodeConfig.CatalogPath = vdb.CatalogPrefix + nodeConfig.CatalogPath = vdb.GenCatalogPath(vnode.Name) } if vdb.DataPrefix == "" && len(vnode.StorageLocations) > 0 { - nodeConfig.DataPath = util.GetPathPrefix(vnode.StorageLocations[0]) + nodeConfig.DataPath = vnode.StorageLocations[0] } else { - nodeConfig.DataPath = vdb.DataPrefix + nodeConfig.DataPath = vdb.GenDataPath(vnode.Name) } if vdb.IsEon && vdb.DepotPrefix == "" { - nodeConfig.DepotPath = util.GetPathPrefix(vnode.DepotPath) - } else { - nodeConfig.DepotPath = vdb.DepotPrefix + nodeConfig.DepotPath = vnode.DepotPath + } else if vdb.DepotPrefix != "" { + nodeConfig.DepotPath = vdb.GenDepotPath(vnode.Name) } dbConfig.Nodes = append(dbConfig.Nodes, &nodeConfig) @@ -255,6 +255,7 @@ func readVDBToDBConfig(vdb *vclusterops.VCoordinationDatabase) (DatabaseConfig, dbConfig.CommunalStorageLocation = vdb.CommunalStorageLocation dbConfig.Ipv6 = vdb.Ipv6 dbConfig.Name = vdb.Name + dbConfig.FirstStartAfterRevive = vdb.FirstStartAfterRevive return dbConfig, nil } @@ -320,5 +321,5 @@ func (c *DatabaseConfig) getPathPrefixes() (catalogPrefix string, return "", "", "" } - return c.Nodes[0].CatalogPath, c.Nodes[0].DataPath, c.Nodes[0].DepotPath + return util.GetPathPrefix(c.Nodes[0].CatalogPath), util.GetPathPrefix(c.Nodes[0].DataPath), util.GetPathPrefix(c.Nodes[0].DepotPath) } diff --git a/rfc7807/errors.go b/rfc7807/errors.go index bb4b6e0..d488cd8 100644 --- a/rfc7807/errors.go +++ b/rfc7807/errors.go @@ -202,9 +202,14 @@ var ( "Target path does not exist", http.StatusBadRequest, ) - CECatalogDirEmptyError = newProblemID( - path.Join(errorEndpointsPrefix, "catalog-dir-empty-error"), + CECatalogContentDirEmptyError = newProblemID( + path.Join(errorEndpointsPrefix, "catalog-content-dir-empty-error"), "Target directory is empty", http.StatusInternalServerError, ) + CECatalogContentDirNotExistError = newProblemID( + path.Join(errorEndpointsPrefix, "catalog-content-dir-not-exist-error"), + "Target directory does not exist", + http.StatusInternalServerError, + ) ) diff --git a/vclusterops/add_node.go b/vclusterops/add_node.go index adb4049..614ff68 100644 --- a/vclusterops/add_node.go +++ b/vclusterops/add_node.go @@ -49,72 +49,89 @@ type VAddNodeOptions struct { } func VAddNodeOptionsFactory() VAddNodeOptions { - opt := VAddNodeOptions{} + options := VAddNodeOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } -func (o *VAddNodeOptions) setDefaultValues() { - o.DatabaseOptions.setDefaultValues() +func (options *VAddNodeOptions) setDefaultValues() { + options.DatabaseOptions.setDefaultValues() - o.SkipRebalanceShards = new(bool) + options.SkipRebalanceShards = new(bool) } -func (o *VAddNodeOptions) validateEonOptions() error { - if o.DepotPrefix != "" { - return util.ValidateRequiredAbsPath(o.DepotPrefix, "depot path") +func (options *VAddNodeOptions) validateEonOptions() error { + if options.DepotPrefix != "" { + return util.ValidateRequiredAbsPath(options.DepotPrefix, "depot path") } return nil } -func (o *VAddNodeOptions) validateExtraOptions() error { +func (options *VAddNodeOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandAddNode, logger) + if err != nil { + return err + } + return nil +} + +func (options *VAddNodeOptions) validateExtraOptions() error { // data prefix - if o.DataPrefix != "" { - return util.ValidateRequiredAbsPath(o.DataPrefix, "data path") + if options.DataPrefix != "" { + return util.ValidateRequiredAbsPath(options.DataPrefix, "data path") + } + + err := util.ValidateScName(options.SCName) + if err != nil { + return err } return nil } -func (o *VAddNodeOptions) validateParseOptions(logger vlog.Printer) error { +func (options *VAddNodeOptions) validateParseOptions(logger vlog.Printer) error { // batch 1: validate required parameters - err := o.validateBaseOptions("db_add_node", logger) + err := options.validateRequiredOptions(logger) if err != nil { return err } // batch 2: validate all other params - return o.validateExtraOptions() + err = options.validateExtraOptions() + if err != nil { + return err + } + return nil } // analyzeOptions will modify some options based on what is chosen -func (o *VAddNodeOptions) analyzeOptions() (err error) { - o.NewHosts, err = util.ResolveRawHostsToAddresses(o.NewHosts, o.IPv6) +func (options *VAddNodeOptions) analyzeOptions() (err error) { + options.NewHosts, err = util.ResolveRawHostsToAddresses(options.NewHosts, options.IPv6) if err != nil { return err } // we analyze host names when it is set in user input, otherwise we use hosts in yaml config // resolve RawHosts to be IP addresses - if len(o.RawHosts) > 0 { - o.Hosts, err = util.ResolveRawHostsToAddresses(o.RawHosts, o.IPv6) + if len(options.RawHosts) > 0 { + options.Hosts, err = util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - o.normalizePaths() + options.normalizePaths() } return nil } -func (o *VAddNodeOptions) validateAnalyzeOptions(logger vlog.Printer) error { - err := o.validateParseOptions(logger) +func (options *VAddNodeOptions) validateAnalyzeOptions(logger vlog.Printer) error { + err := options.validateParseOptions(logger) if err != nil { return err } - return o.analyzeOptions() + return options.analyzeOptions() } // VAddNode adds one or more nodes to an existing database. @@ -195,18 +212,18 @@ func checkAddNodeRequirements(vdb *VCoordinationDatabase, hostsToAdd []string) e // completeVDBSetting sets some VCoordinationDatabase fields we cannot get yet // from the https endpoints. We set those fields from options. -func (o *VAddNodeOptions) completeVDBSetting(vdb *VCoordinationDatabase) error { - vdb.DataPrefix = o.DataPrefix - vdb.DepotPrefix = o.DepotPrefix +func (options *VAddNodeOptions) completeVDBSetting(vdb *VCoordinationDatabase) error { + vdb.DataPrefix = options.DataPrefix + vdb.DepotPrefix = options.DepotPrefix hostNodeMap := makeVHostNodeMap() // TODO: we set the depot and data path from /nodes rather than manually // (VER-92725). This is useful for nmaDeleteDirectoriesOp. for h, vnode := range vdb.HostNodeMap { - dataPath := vdb.genDataPath(vnode.Name) + dataPath := vdb.GenDataPath(vnode.Name) vnode.StorageLocations = append(vnode.StorageLocations, dataPath) if vdb.DepotPrefix != "" { - vnode.DepotPath = vdb.genDepotPath(vnode.Name) + vnode.DepotPath = vdb.GenDepotPath(vnode.Name) } hostNodeMap[h] = vnode } @@ -428,11 +445,11 @@ func (vcc VClusterCommands) prepareAdditionalEonInstructions(vdb *VCoordinationD } // setInitiator sets the initiator as the first primary up node -func (o *VAddNodeOptions) setInitiator(primaryUpNodes []string) error { +func (options *VAddNodeOptions) setInitiator(primaryUpNodes []string) error { initiatorHost, err := getInitiatorHost(primaryUpNodes, []string{}) if err != nil { return err } - o.Initiator = initiatorHost + options.Initiator = initiatorHost return nil } diff --git a/vclusterops/add_subcluster.go b/vclusterops/add_subcluster.go index d473e76..b6dbcad 100644 --- a/vclusterops/add_subcluster.go +++ b/vclusterops/add_subcluster.go @@ -55,13 +55,13 @@ type VAddSubclusterInfo struct { } func VAddSubclusterOptionsFactory() VAddSubclusterOptions { - opt := VAddSubclusterOptions{} + options := VAddSubclusterOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() // set default values for VAddNodeOptions - opt.VAddNodeOptions.setDefaultValues() + options.VAddNodeOptions.setDefaultValues() - return opt + return options } func (options *VAddSubclusterOptions) setDefaultValues() { @@ -71,7 +71,7 @@ func (options *VAddSubclusterOptions) setDefaultValues() { } func (options *VAddSubclusterOptions) validateRequiredOptions(logger vlog.Printer) error { - err := options.validateBaseOptions("db_add_subcluster", logger) + err := options.validateBaseOptions(commandAddSubcluster, logger) if err != nil { return err } @@ -79,6 +79,12 @@ func (options *VAddSubclusterOptions) validateRequiredOptions(logger vlog.Printe if options.SCName == "" { return fmt.Errorf("must specify a subcluster name") } + + err = util.ValidateScName(options.SCName) + if err != nil { + return err + } + return nil } @@ -125,9 +131,9 @@ func (options *VAddSubclusterOptions) validateExtraOptions(logger vlog.Printer) return nil } -func (options *VAddSubclusterOptions) validateParseOptions(vcc VClusterCommands) error { +func (options *VAddSubclusterOptions) validateParseOptions(logger vlog.Printer) error { // batch 1: validate required parameters - err := options.validateRequiredOptions(vcc.Log) + err := options.validateRequiredOptions(logger) if err != nil { return err } @@ -137,7 +143,7 @@ func (options *VAddSubclusterOptions) validateParseOptions(vcc VClusterCommands) return err } // batch 3: validate all other params - err = options.validateExtraOptions(vcc.Log) + err = options.validateExtraOptions(logger) if err != nil { return err } @@ -166,28 +172,31 @@ func (options *VAddSubclusterOptions) analyzeOptions() (err error) { return nil } -func (options *VAddSubclusterOptions) validateAnalyzeOptions(vcc VClusterCommands) error { - if err := options.validateParseOptions(vcc); err != nil { +func (options *VAddSubclusterOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } err := options.analyzeOptions() if err != nil { return err } - return options.setUsePassword(vcc.Log) + return options.setUsePassword(logger) } // VAddSubcluster adds to a running database a new subcluster with provided options. // It returns any error encountered. +// +//nolint:dupl func (vcc VClusterCommands) VAddSubcluster(options *VAddSubclusterOptions) error { /* + * - Validate Options * - Produce Instructions * - Create a VClusterOpEngine * - Give the instructions to the VClusterOpEngine to run */ // validate and analyze all options - err := options.validateAnalyzeOptions(vcc) + err := options.validateAnalyzeOptions(vcc.Log) if err != nil { return err } @@ -215,24 +224,27 @@ func (vcc VClusterCommands) VAddSubcluster(options *VAddSubclusterOptions) error // // The generated instructions will later perform the following operations necessary // for a successful add_subcluster: -// - TODO: add nma connectivity check and nma version check // - Get cluster info from running db and exit error if the db is an enterprise db // - Get UP nodes through HTTPS call, if any node is UP then the DB is UP and ready for adding a new subcluster // - Add the subcluster catalog object through HTTPS call, and check the response to error out // if the subcluster name already exists // - Check if the new subcluster is created in database through HTTPS call -// - TODO: add new nodes to the subcluster func (vcc *VClusterCommands) produceAddSubclusterInstructions(options *VAddSubclusterOptions) ([]clusterOp, error) { var instructions []clusterOp vdb := makeVCoordinationDatabase() + // NMA health check + // this is not needed for adding subcluster + // but if this failed, adding nodes may fail and may give users confusing messages + nmaHealthOp := makeNMAHealthOp(options.Hosts) + // get cluster info err := vcc.getClusterInfoFromRunningDB(&vdb, &options.DatabaseOptions) if err != nil { return instructions, err } - // db_add_subcluster only works with Eon database + // add_subcluster only works with Eon database if !vdb.IsEon { // info from running db confirms that the db is not Eon return instructions, fmt.Errorf("add subcluster is only supported in Eon mode") @@ -258,6 +270,7 @@ func (vcc *VClusterCommands) produceAddSubclusterInstructions(options *VAddSubcl } instructions = append(instructions, + &nmaHealthOp, &httpsGetUpNodesOp, &httpsAddSubclusterOp, &httpsCheckSubclusterOp, diff --git a/vclusterops/alter_subcluster_type.go b/vclusterops/alter_subcluster_type.go new file mode 100644 index 0000000..3a0ee9b --- /dev/null +++ b/vclusterops/alter_subcluster_type.go @@ -0,0 +1,195 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "fmt" + + "github.com/vertica/vcluster/vclusterops/util" + "github.com/vertica/vcluster/vclusterops/vlog" +) + +type SubclusterType string + +const ( + Primary SubclusterType = "primary" + Secondary SubclusterType = "secondary" +) + +func (s SubclusterType) IsValid() bool { + switch s { + case Primary, Secondary: + return true + } + return false +} + +type VAlterSubclusterTypeOptions struct { + // Basic db info + DatabaseOptions + // Name of the subcluster to promote or demote in sandbox or main cluster + SCName string + // Type of the subcluster to promote or demote + // Set to primary to demote the subcluster. + // Set to secondary to promote the subcluster. + SCType SubclusterType + // Name of the sandbox + // Use this option when promoting or demoting a subcluster in a sandbox. + // If this option is not set, the subcluster will be promoted or demoted in the main cluster. + Sandbox string +} + +func VPromoteDemoteFactory() VAlterSubclusterTypeOptions { + options := VAlterSubclusterTypeOptions{} + // set default values to the params + options.setDefaultValues() + return options +} + +func (options *VAlterSubclusterTypeOptions) validateEonOptions(_ vlog.Printer) error { + if !options.IsEon { + return fmt.Errorf("promote or demote subclusters are only supported in Eon mode") + } + return nil +} + +func (options *VAlterSubclusterTypeOptions) validateParseOptions(logger vlog.Printer) error { + err := options.validateEonOptions(logger) + if err != nil { + return err + } + + // need to provide a password or certs + if options.Password == nil && (options.Cert == "" || options.Key == "") { + return fmt.Errorf("must provide a password or certs") + } + + if options.SCName == "" { + return fmt.Errorf("must specify a subcluster name") + } + + err = util.ValidateScName(options.SCName) + if err != nil { + return err + } + + if !options.SCType.IsValid() { + return fmt.Errorf("invalid subcluster type: must be 'primary' or 'secondary'") + } + return options.validateBaseOptions(commandAlterSubclusterType, logger) +} + +// analyzeOptions will modify some options based on what is chosen +func (options *VAlterSubclusterTypeOptions) analyzeOptions() (err error) { + if len(options.RawHosts) > 0 { + // resolve RawHosts to be IP addresses + options.Hosts, err = util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) + if err != nil { + return err + } + } + return nil +} + +func (options *VAlterSubclusterTypeOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { + return err + } + return options.analyzeOptions() +} + +// VAlterSubclusterType can promote/demote subcluster to different types +func (vcc VClusterCommands) VAlterSubclusterType(options *VAlterSubclusterTypeOptions) error { + /* + * - Produce Instructions + * - Create a VClusterOpEngine + * - Give the instructions to the VClusterOpEngine to run + */ + + // validate and analyze options + err := options.validateAnalyzeOptions(vcc.Log) + if err != nil { + return err + } + + // retrieve information from the database to accurately determine the state of each node in both the main cluster and sandbox + vdb := makeVCoordinationDatabase() + err = vcc.getVDBFromRunningDBIncludeSandbox(&vdb, &options.DatabaseOptions, options.Sandbox) + if err != nil { + return err + } + + // produce alter subcluster type instructions + instructions, err := vcc.produceAlterSubclusterTypeInstructions(options, &vdb) + if err != nil { + return fmt.Errorf("fail to produce instructions, %w", err) + } + + // create a VClusterOpEngine, and add certs to the engine + certs := httpsCerts{key: options.Key, cert: options.Cert, caCert: options.CaCert} + clusterOpEngine := makeClusterOpEngine(instructions, &certs) + + // give the instructions to the VClusterOpEngine to run + runError := clusterOpEngine.run(vcc.Log) + if runError != nil { + if options.SCType == Secondary { + return fmt.Errorf("fail to promote subcluster: %w", runError) + } + if options.SCType == Primary { + return fmt.Errorf("fail to demote subcluster: %w", runError) + } + } + + return nil +} + +// The generated instructions will later perform the following operations necessary +// for a successful alter subcluster type operation: +// - Promote subclusters using one of the up nodes in the main subcluster or a sandbox other than the target subcluster +// and subcluster type is secondary +// - Demote subclusters using one of the up nodes in the main subcluster or a sandbox other than the target subcluster +// and subcluster type is primary +func (vcc VClusterCommands) produceAlterSubclusterTypeInstructions(options *VAlterSubclusterTypeOptions, + vdb *VCoordinationDatabase) ([]clusterOp, error) { + var instructions []clusterOp + + // need username for https operations + err := options.setUsePassword(vcc.Log) + if err != nil { + return instructions, err + } + + var noHosts = []string{} // We pass in no hosts so that this op picks an up node from the previous call. + if options.SCType == Secondary { + httpsPromoteScOp, err := makeHTTPSPromoteSubclusterOp(noHosts, options.usePassword, + options.UserName, options.Password, options.SCName, options.Sandbox, vdb) + if err != nil { + return nil, err + } + instructions = append(instructions, &httpsPromoteScOp) + } else if options.SCType == Primary { + httpsDemoteScOp, err := makeHTTPSDemoteSubclusterOp(noHosts, options.usePassword, + options.UserName, options.Password, options.SCName, options.Sandbox, vdb) + if err != nil { + return nil, err + } + instructions = append(instructions, &httpsDemoteScOp) + } else { + return nil, fmt.Errorf("failed to add instructions: unsupported subcluster type '%s'", options.SCType) + } + + return instructions, nil +} diff --git a/vclusterops/alter_subcluster_type_test.go b/vclusterops/alter_subcluster_type_test.go new file mode 100644 index 0000000..d7b8310 --- /dev/null +++ b/vclusterops/alter_subcluster_type_test.go @@ -0,0 +1,62 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vertica/vcluster/vclusterops/vlog" +) + +func TestVAlterSubclusterTypeOptions_validateParseOptions(t *testing.T) { + logger := vlog.Printer{} + + opt := VPromoteDemoteFactory() + testPassword := "test-password-1" + + opt.SCName = testSCName + opt.IsEon = true + opt.RawHosts = append(opt.RawHosts, "test-raw-host") + opt.DBName = testDBName + opt.UserName = testUserName + opt.Password = &testPassword + opt.SCType = Primary + + err := opt.validateParseOptions(logger) + assert.NoError(t, err) + + opt.UserName = "" + err = opt.validateParseOptions(logger) + assert.NoError(t, err) + + // negative: no database name + opt.UserName = testUserName + opt.DBName = "" + err = opt.validateParseOptions(logger) + assert.ErrorContains(t, err, "must specify a database name") + + // negative: no subcluster name + opt.DBName = testDBName + opt.SCName = "" + err = opt.validateParseOptions(logger) + assert.ErrorContains(t, err, "must specify a subcluster name") + + // negative: enterprise database + opt.IsEon = false + err = opt.validateParseOptions(logger) + assert.ErrorContains(t, err, "promote or demote subclusters are only supported in Eon mode") +} diff --git a/vclusterops/cluster_op.go b/vclusterops/cluster_op.go index ac04dc4..dc55fbd 100644 --- a/vclusterops/cluster_op.go +++ b/vclusterops/cluster_op.go @@ -46,6 +46,7 @@ const ( SUCCESS resultStatus = 0 FAILURE resultStatus = 1 EXCEPTION resultStatus = 2 + EOF resultStatus = 3 ) const ( @@ -83,7 +84,7 @@ type hostHTTPResult struct { statusCode int host string content string - err error // This is set if the http response ends in a failure scenario + err error // This is set if the http response with a status code that is not 2XX } type httpsResponseStatus struct { @@ -133,6 +134,7 @@ func (hostResult *hostHTTPResult) isHTTPRunning() bool { return false } +// HTTP status code >= 200 and < 300 func (hostResult *hostHTTPResult) isPassing() bool { return hostResult.err == nil } @@ -155,6 +157,10 @@ func (hostResult *hostHTTPResult) isTimeout() bool { return false } +func (hostResult *hostHTTPResult) isEOF() bool { + return hostResult.status == EOF +} + // getStatusString converts ResultStatus to string func (status resultStatus) getStatusString() string { if status == FAILURE { @@ -506,6 +512,8 @@ type ClusterCommands interface { VFetchCoordinationDatabase(options *VFetchCoordinationDatabaseOptions) (VCoordinationDatabase, error) VUnsandbox(options *VUnsandboxOptions) error VStopSubcluster(options *VStopSubclusterOptions) error + VAlterSubclusterType(options *VAlterSubclusterTypeOptions) error + VRenameSubcluster(options *VRenameSubclusterOptions) error VFetchNodesDetails(options *VFetchNodesDetailsOptions) (NodesDetails, error) } diff --git a/vclusterops/coordinator_database.go b/vclusterops/coordinator_database.go index e5722c8..ad2dd12 100644 --- a/vclusterops/coordinator_database.go +++ b/vclusterops/coordinator_database.go @@ -55,7 +55,8 @@ type VCoordinationDatabase struct { // more to add when useful Ipv6 bool - PrimaryUpNodes []string + PrimaryUpNodes []string + FirstStartAfterRevive bool } type vHostNodeMap map[string]*VCoordinationNode @@ -266,20 +267,20 @@ func (vdb *VCoordinationDatabase) hasAtLeastOneDownNode() bool { return false } -// genDataPath builds and returns the data path -func (vdb *VCoordinationDatabase) genDataPath(nodeName string) string { +// GenDataPath builds and returns the data path +func (vdb *VCoordinationDatabase) GenDataPath(nodeName string) string { dataSuffix := fmt.Sprintf("%s_data", nodeName) return filepath.Join(vdb.DataPrefix, vdb.Name, dataSuffix) } -// genDepotPath builds and returns the depot path -func (vdb *VCoordinationDatabase) genDepotPath(nodeName string) string { +// GenDepotPath builds and returns the depot path +func (vdb *VCoordinationDatabase) GenDepotPath(nodeName string) string { depotSuffix := fmt.Sprintf("%s_depot", nodeName) return filepath.Join(vdb.DepotPrefix, vdb.Name, depotSuffix) } -// genCatalogPath builds and returns the catalog path -func (vdb *VCoordinationDatabase) genCatalogPath(nodeName string) string { +// GenCatalogPath builds and returns the catalog path +func (vdb *VCoordinationDatabase) GenCatalogPath(nodeName string) string { catalogSuffix := fmt.Sprintf("%s_catalog", nodeName) return filepath.Join(vdb.CatalogPrefix, vdb.Name, catalogSuffix) } @@ -382,11 +383,11 @@ func (vnode *VCoordinationNode) setNode(vdb *VCoordinationDatabase, address, nam vnode.Address = address vnode.Name = name vnode.Subcluster = scName - vnode.CatalogPath = vdb.genCatalogPath(vnode.Name) - dataPath := vdb.genDataPath(vnode.Name) + vnode.CatalogPath = vdb.GenCatalogPath(vnode.Name) + dataPath := vdb.GenDataPath(vnode.Name) vnode.StorageLocations = append(vnode.StorageLocations, dataPath) if vdb.DepotPrefix != "" { - vnode.DepotPath = vdb.genDepotPath(vnode.Name) + vnode.DepotPath = vdb.GenDepotPath(vnode.Name) } if vdb.Ipv6 { vnode.ControlAddressFamily = util.IPv6ControlAddressFamily diff --git a/vclusterops/create_db.go b/vclusterops/create_db.go index fc2f806..bdbbe2a 100644 --- a/vclusterops/create_db.go +++ b/vclusterops/create_db.go @@ -72,38 +72,44 @@ type VCreateDatabaseOptions struct { } func VCreateDatabaseOptionsFactory() VCreateDatabaseOptions { - opt := VCreateDatabaseOptions{} + options := VCreateDatabaseOptions{} // set default values to the params - opt.setDefaultValues() - return opt + options.setDefaultValues() + return options } -func (opt *VCreateDatabaseOptions) setDefaultValues() { - opt.DatabaseOptions.setDefaultValues() +func (options *VCreateDatabaseOptions) setDefaultValues() { + options.DatabaseOptions.setDefaultValues() // basic db info defaultPolicy := util.DefaultRestartPolicy - opt.Policy = defaultPolicy + options.Policy = defaultPolicy // optional info - opt.TimeoutNodeStartupSeconds = util.DefaultTimeoutSeconds + options.TimeoutNodeStartupSeconds = util.DefaultTimeoutSeconds // new params originally in installer generated admintools.conf, now in create db op - opt.P2p = util.DefaultP2p - opt.LargeCluster = util.DefaultLargeCluster - opt.ClientPort = util.DefaultClientPort - opt.SpreadLoggingLevel = util.DefaultSpreadLoggingLevel + options.P2p = util.DefaultP2p + options.LargeCluster = util.DefaultLargeCluster + options.ClientPort = util.DefaultClientPort + options.SpreadLoggingLevel = util.DefaultSpreadLoggingLevel } -func (opt *VCreateDatabaseOptions) validateRequiredOptions(logger vlog.Printer) error { +func (options *VCreateDatabaseOptions) validateRequiredOptions(logger vlog.Printer) error { + // validate base options + err := options.validateBaseOptions(commandCreateDB, logger) + if err != nil { + return err + } + // validate required parameters with default values - if opt.Password == nil { - opt.Password = new(string) - *opt.Password = "" + if options.Password == nil { + options.Password = new(string) + *options.Password = "" logger.Info("no password specified, using none") } - if !util.StringInArray(opt.Policy, util.RestartPolicyList) { + if !util.StringInArray(options.Policy, util.RestartPolicyList) { return fmt.Errorf("policy must be one of %v", util.RestartPolicyList) } @@ -114,7 +120,7 @@ func (opt *VCreateDatabaseOptions) validateRequiredOptions(logger vlog.Printer) // // empty string ("") will be converted to the default license path (/opt/vertica/share/license.key) // in the /bootstrap-catalog endpoint - if opt.LicensePathOnNode != "" && !util.IsAbsPath(opt.LicensePathOnNode) { + if options.LicensePathOnNode != "" && !util.IsAbsPath(options.LicensePathOnNode) { return fmt.Errorf("must provide a fully qualified path for license file") } @@ -190,30 +196,30 @@ func validateDepotSize(size string) (bool, error) { return true, nil } -func (opt *VCreateDatabaseOptions) validateEonOptions() error { - if opt.CommunalStorageLocation != "" { - err := util.ValidateCommunalStorageLocation(opt.CommunalStorageLocation) +func (options *VCreateDatabaseOptions) validateEonOptions() error { + if options.CommunalStorageLocation != "" { + err := util.ValidateCommunalStorageLocation(options.CommunalStorageLocation) if err != nil { return err } - if opt.DepotPrefix == "" { + if options.DepotPrefix == "" { return fmt.Errorf("must specify a depot path with commual storage location") } - if opt.ShardCount == 0 { + if options.ShardCount == 0 { return fmt.Errorf("must specify a shard count greater than 0 with communal storage location") } } - if opt.DepotPrefix != "" && opt.CommunalStorageLocation == "" { + if options.DepotPrefix != "" && options.CommunalStorageLocation == "" { return fmt.Errorf("when depot path is given, communal storage location cannot be empty") } - if opt.GetAwsCredentialsFromEnv && opt.CommunalStorageLocation == "" { + if options.GetAwsCredentialsFromEnv && options.CommunalStorageLocation == "" { return fmt.Errorf("AWS credentials are only used in Eon mode") } - if opt.DepotSize != "" { - if opt.DepotPrefix == "" { + if options.DepotSize != "" { + if options.DepotPrefix == "" { return fmt.Errorf("when depot size is given, depot path cannot be empty") } - validDepotSize, err := validateDepotSize(opt.DepotSize) + validDepotSize, err := validateDepotSize(options.DepotSize) if !validDepotSize { return err } @@ -221,36 +227,30 @@ func (opt *VCreateDatabaseOptions) validateEonOptions() error { return nil } -func (opt *VCreateDatabaseOptions) validateExtraOptions() error { - if opt.Broadcast && opt.P2p { +func (options *VCreateDatabaseOptions) validateExtraOptions() error { + if options.Broadcast && options.P2p { return fmt.Errorf("cannot use both Broadcast and Point-to-point networking mode") } // -1 is the default large cluster value, meaning 120 control nodes - if opt.LargeCluster != util.DefaultLargeCluster && (opt.LargeCluster < 1 || opt.LargeCluster > util.MaxLargeCluster) { + if options.LargeCluster != util.DefaultLargeCluster && (options.LargeCluster < 1 || options.LargeCluster > util.MaxLargeCluster) { return fmt.Errorf("must specify a valid large cluster value in range [1, 120]") } return nil } -func (opt *VCreateDatabaseOptions) validateParseOptions(logger vlog.Printer) error { - // validate base options - err := opt.validateBaseOptions("create_db", logger) - if err != nil { - return err - } - +func (options *VCreateDatabaseOptions) validateParseOptions(logger vlog.Printer) error { // batch 1: validate required parameters without default values - err = opt.validateRequiredOptions(logger) + err := options.validateRequiredOptions(logger) if err != nil { return err } // batch 2: validate eon params - err = opt.validateEonOptions() + err = options.validateEonOptions() if err != nil { return err } // batch 3: validate all other params - err = opt.validateExtraOptions() + err = options.validateExtraOptions() if err != nil { return err } @@ -258,29 +258,29 @@ func (opt *VCreateDatabaseOptions) validateParseOptions(logger vlog.Printer) err } // Do advanced analysis on the options inputs, like resolve hostnames to be IPs -func (opt *VCreateDatabaseOptions) analyzeOptions() error { +func (options *VCreateDatabaseOptions) analyzeOptions() error { // resolve RawHosts to be IP addresses - if len(opt.RawHosts) > 0 { - hostAddresses, err := util.ResolveRawHostsToAddresses(opt.RawHosts, opt.IPv6) + if len(options.RawHosts) > 0 { + hostAddresses, err := util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - opt.Hosts = hostAddresses + options.Hosts = hostAddresses } // process correct catalog path, data path and depot path prefixes - opt.CatalogPrefix = util.GetCleanPath(opt.CatalogPrefix) - opt.DataPrefix = util.GetCleanPath(opt.DataPrefix) - opt.DepotPrefix = util.GetCleanPath(opt.DepotPrefix) + options.CatalogPrefix = util.GetCleanPath(options.CatalogPrefix) + options.DataPrefix = util.GetCleanPath(options.DataPrefix) + options.DepotPrefix = util.GetCleanPath(options.DepotPrefix) return nil } -func (opt *VCreateDatabaseOptions) validateAnalyzeOptions(logger vlog.Printer) error { - if err := opt.validateParseOptions(logger); err != nil { +func (options *VCreateDatabaseOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } - return opt.analyzeOptions() + return options.analyzeOptions() } func (vcc VClusterCommands) VCreateDatabase(options *VCreateDatabaseOptions) (VCoordinationDatabase, error) { @@ -470,11 +470,6 @@ func (vcc VClusterCommands) produceCreateDBWorkerNodesInstructions( } instructions = append(instructions, &httpsReloadSpreadOp) - hostNodeMap := make(map[string]string) - for _, host := range hosts { - hostNodeMap[host] = vdb.HostNodeMap[host].CatalogPath - } - if len(hosts) > 1 { httpsGetNodesInfoOp, err := makeHTTPSGetNodesInfoOp(options.DBName, bootstrapHost, true /* use password auth */, options.UserName, options.Password, vdb, false, util.MainClusterSandbox) diff --git a/vclusterops/drop_db.go b/vclusterops/drop_db.go index e80bcd1..adcaa49 100644 --- a/vclusterops/drop_db.go +++ b/vclusterops/drop_db.go @@ -28,11 +28,11 @@ type VDropDatabaseOptions struct { } func VDropDatabaseOptionsFactory() VDropDatabaseOptions { - opt := VDropDatabaseOptions{} + options := VDropDatabaseOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } // analyzeOptions verifies the host options for the VDropDatabaseOptions struct and @@ -49,10 +49,24 @@ func (options *VDropDatabaseOptions) analyzeOptions() error { return nil } -func (options *VDropDatabaseOptions) validateAnalyzeOptions() error { +func (options *VDropDatabaseOptions) validateParseOptions() error { if options.DBName == "" { return fmt.Errorf("database name must be provided") } + + err := util.ValidateDBName(options.DBName) + if err != nil { + return err + } + return nil +} + +func (options *VDropDatabaseOptions) validateAnalyzeOptions() error { + err := options.validateParseOptions() + if err != nil { + return err + } + return options.analyzeOptions() } @@ -121,7 +135,7 @@ func (vcc VClusterCommands) produceDropDBInstructions(vdb *VCoordinationDatabase // when checking the running database, // drop_db has the same checking items with create_db checkDBRunningOp, err := makeHTTPSCheckRunningDBOp(hosts, usePassword, - options.UserName, options.Password, CreateDB) + options.UserName, options.Password, DropDB) if err != nil { return instructions, err } diff --git a/vclusterops/fetch_database.go b/vclusterops/fetch_database.go index ff46ebb..984b1fd 100644 --- a/vclusterops/fetch_database.go +++ b/vclusterops/fetch_database.go @@ -24,52 +24,53 @@ import ( type VFetchCoordinationDatabaseOptions struct { DatabaseOptions - Overwrite bool // overwrite existing config file at the same location + Overwrite bool // overwrite existing config file at the same location + AfterRevive bool // whether recover config right after revive_db // hidden option readOnly bool // this should be only used if we don't want to update the config file } func VRecoverConfigOptionsFactory() VFetchCoordinationDatabaseOptions { - opt := VFetchCoordinationDatabaseOptions{} + options := VFetchCoordinationDatabaseOptions{} // set default values to the params - opt.setDefaultValues() - return opt + options.setDefaultValues() + return options } -func (opt *VFetchCoordinationDatabaseOptions) validateParseOptions(logger vlog.Printer) error { - return opt.validateBaseOptions(commandConfigRecover, logger) +func (options *VFetchCoordinationDatabaseOptions) validateParseOptions(logger vlog.Printer) error { + return options.validateBaseOptions(commandConfigRecover, logger) } -func (opt *VFetchCoordinationDatabaseOptions) analyzeOptions() error { +func (options *VFetchCoordinationDatabaseOptions) analyzeOptions() error { // resolve RawHosts to be IP addresses - if len(opt.RawHosts) > 0 { - hostAddresses, err := util.ResolveRawHostsToAddresses(opt.RawHosts, opt.IPv6) + if len(options.RawHosts) > 0 { + hostAddresses, err := util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - opt.Hosts = hostAddresses + options.Hosts = hostAddresses } // process correct catalog path - opt.CatalogPrefix = util.GetCleanPath(opt.CatalogPrefix) + options.CatalogPrefix = util.GetCleanPath(options.CatalogPrefix) // check existing config file at the same location - if !opt.readOnly && !opt.Overwrite { - if util.CanWriteAccessPath(opt.ConfigPath) == util.FileExist { + if !options.readOnly && !options.Overwrite { + if util.CanWriteAccessPath(options.ConfigPath) == util.FileExist { return fmt.Errorf("config file exists at %s. "+ - "You can use --overwrite to overwrite this existing config file", opt.ConfigPath) + "You can use --overwrite to overwrite this existing config file", options.ConfigPath) } } return nil } -func (opt *VFetchCoordinationDatabaseOptions) validateAnalyzeOptions(logger vlog.Printer) error { - if err := opt.validateParseOptions(logger); err != nil { +func (options *VFetchCoordinationDatabaseOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } - return opt.analyzeOptions() + return options.analyzeOptions() } func (vcc VClusterCommands) VFetchCoordinationDatabase(options *VFetchCoordinationDatabaseOptions) (VCoordinationDatabase, error) { @@ -93,7 +94,7 @@ func (vcc VClusterCommands) VFetchCoordinationDatabase(options *VFetchCoordinati vdb.DepotPrefix = options.DepotPrefix vdb.Ipv6 = options.IPv6 - // produce list_allnodes instructions + // produce list_all_nodes instructions instructions, err := vcc.produceRecoverConfigInstructions(options, &vdb) if err != nil { return vdb, fmt.Errorf("fail to produce instructions, %w", err) @@ -148,22 +149,27 @@ func (vcc VClusterCommands) produceRecoverConfigInstructions( var instructions []clusterOp nmaHealthOp := makeNMAHealthOp(options.Hosts) + instructions = append(instructions, &nmaHealthOp) - nmaGetNodesInfoOp := makeNMAGetNodesInfoOp(options.Hosts, options.DBName, options.CatalogPrefix, - true /* ignore internal errors */, vdb) - - nmaReadCatalogEditorOp, err := makeNMAReadCatalogEditorOp(vdb) + // Try fetching nodes info from a running db, if possible. + err := vcc.getVDBFromRunningDBIncludeSandbox(vdb, &options.DatabaseOptions, AnySandbox) if err != nil { - return instructions, err + vcc.PrintWarning("No running db found. For eon db, restart the database to recover accurate sandbox information") + nmaGetNodesInfoOp := makeNMAGetNodesInfoOp(options.Hosts, options.DBName, options.CatalogPrefix, + true /* ignore internal errors */, vdb) + nmaReadCatalogEditorOp, err := makeNMAReadCatalogEditorOp(vdb) + if err != nil { + return instructions, err + } + instructions = append( + instructions, + &nmaGetNodesInfoOp, + &nmaReadCatalogEditorOp) } - nmaReadVerticaVersionOp := makeNMAReadVerticaVersionOp(vdb) instructions = append( instructions, - &nmaHealthOp, - &nmaGetNodesInfoOp, - &nmaReadCatalogEditorOp, &nmaReadVerticaVersionOp, ) diff --git a/vclusterops/fetch_node_state.go b/vclusterops/fetch_node_state.go index 2889b17..06cc65c 100644 --- a/vclusterops/fetch_node_state.go +++ b/vclusterops/fetch_node_state.go @@ -68,12 +68,13 @@ func (vcc VClusterCommands) VFetchNodeState(options *VFetchNodeStateOptions) ([] // this vdb is used to fetch node version var vdb VCoordinationDatabase - err = vcc.getVDBFromRunningDB(&vdb, &options.DatabaseOptions) + err = vcc.getVDBFromRunningDBIncludeSandbox(&vdb, &options.DatabaseOptions, util.MainClusterSandbox) if err != nil { + vcc.Log.PrintInfo("Error from vdb build: %s", err.Error()) return vcc.fetchNodeStateFromDownDB(options) } - // produce list_allnodes instructions + // produce list_all_nodes instructions instructions, err := vcc.produceListAllNodesInstructions(options, &vdb) if err != nil { return nil, fmt.Errorf("fail to produce instructions, %w", err) @@ -181,6 +182,9 @@ func (vcc VClusterCommands) produceListAllNodesInstructions( nmaHealthOp := makeNMAHealthOp(options.Hosts) nmaReadVerticaVersionOp := makeNMAReadVerticaVersionOp(vdb) + // Trim host list + hosts = options.updateHostlist(vcc, vdb, hosts) + httpsCheckNodeStateOp, err := makeHTTPSCheckNodeStateOp(hosts, usePassword, options.UserName, options.Password) if err != nil { @@ -199,3 +203,36 @@ func (vcc VClusterCommands) produceListAllNodesInstructions( return instructions, nil } + +// Update and limit the hostlist based on status and sandbox info +// Note: if we have any UP main cluster host in the input list, the trimmed hostlist would always contain +// +// only main cluster UP hosts. +func (options *VFetchNodeStateOptions) updateHostlist(vcc VClusterCommands, vdb *VCoordinationDatabase, inputHosts []string) []string { + var mainClusterHosts []string + var upSandboxHosts []string + + for _, h := range inputHosts { + vnode, ok := vdb.HostNodeMap[h] + if !ok { + // host address not found in vdb, skip it + continue + } + if vnode.Sandbox == "" && (vnode.State == util.NodeUpState || vnode.State == util.NodeUnknownState) { + mainClusterHosts = append(mainClusterHosts, vnode.Address) + } else if vnode.State == util.NodeUpState { + upSandboxHosts = append(upSandboxHosts, vnode.Address) + } + } + if len(mainClusterHosts) > 0 { + vcc.Log.PrintWarning("Main cluster UP node found in host list. The status would be fetched from a main cluster host!") + return mainClusterHosts + } + if len(upSandboxHosts) > 0 { + vcc.Log.PrintWarning("Only sandboxed UP nodes found in host list. The status would be fetched from a sandbox host!") + return upSandboxHosts + } + + // We do not have an up host, so better try with complete input hostlist + return inputHosts +} diff --git a/vclusterops/fetch_nodes_details.go b/vclusterops/fetch_nodes_details.go index 954b326..304e014 100644 --- a/vclusterops/fetch_nodes_details.go +++ b/vclusterops/fetch_nodes_details.go @@ -73,19 +73,19 @@ type VFetchNodesDetailsOptions struct { } func VFetchNodesDetailsOptionsFactory() VFetchNodesDetailsOptions { - opt := VFetchNodesDetailsOptions{} + options := VFetchNodesDetailsOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } func (options *VFetchNodesDetailsOptions) setDefaultValues() { options.DatabaseOptions.setDefaultValues() } -func (options *VFetchNodesDetailsOptions) validateOptions(log vlog.Printer) error { - err := options.validateBaseOptions(commandFetchNodesDetails, log) +func (options *VFetchNodesDetailsOptions) validateParseOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandFetchNodesDetails, logger) if err != nil { return err } @@ -105,8 +105,8 @@ func (options *VFetchNodesDetailsOptions) analyzeOptions() (err error) { return nil } -func (options *VFetchNodesDetailsOptions) validateAnalyzeOptions(log vlog.Printer) error { - if err := options.validateOptions(log); err != nil { +func (options *VFetchNodesDetailsOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } return options.analyzeOptions() diff --git a/vclusterops/helpers.go b/vclusterops/helpers.go index 308462e..5f082fe 100644 --- a/vclusterops/helpers.go +++ b/vclusterops/helpers.go @@ -154,6 +154,35 @@ func getInitiatorHost(primaryUpNodes, hostsToSkip []string) (string, error) { return initiatorHosts[0], nil } +// getInitiatorHostInCluster returns an initiator that is the first up node of a subcluster in the main cluster +// or a sandbox other than the target subcluster +func getInitiatorHostInCluster(name, sandbox, scname string, vdb *VCoordinationDatabase) ([]string, error) { + // up hosts will be : + // 1. up hosts from the main subcluster if the sandbox is empty + // 2. up hosts from the sandbox if the sandbox is specified + var upHost string + for _, node := range vdb.HostNodeMap { + if node.State == util.NodeDownState { + continue + } + // the up host is used to promote/demote subcluster + // should not be a part of this subcluster + if node.Sandbox == sandbox && node.Subcluster != scname { + upHost = node.Address + break + } + } + if upHost == "" { + if sandbox == "" { + return nil, fmt.Errorf(`[%s] cannot find any up hosts for subcluster %s in main subcluster`, name, scname) + } + return nil, fmt.Errorf("[%s] cannot find any up hosts for subcluster %s in the sandbox %s", name, scname, sandbox) + } + // use first up host to execute https post request + initiatorHost := []string{upHost} + return initiatorHost, nil +} + // getVDBFromRunningDB will retrieve db configurations from a non-sandboxed host by calling https endpoints of a running db func (vcc VClusterCommands) getVDBFromRunningDB(vdb *VCoordinationDatabase, options *DatabaseOptions) error { return vcc.getVDBFromRunningDBImpl(vdb, options, false, util.MainClusterSandbox) @@ -256,6 +285,16 @@ func getInitiator(hosts []string) string { return hosts[0] } +func getInitiatorInSandbox(targetSandbox string, hosts []string, + upHostsToSandboxes map[string]string) (string, error) { + for _, host := range hosts { + if sandbox, ok := upHostsToSandboxes[host]; ok && sandbox == targetSandbox { + return host, nil + } + } + return "", fmt.Errorf("no hosts among %v are both UP and within sandbox %v", hosts, targetSandbox) +} + // getInitiator will pick an initiator from the up host list to execute https calls // such that the initiator is also among the user provided host list func getInitiatorFromUpHosts(upHosts, userProvidedHosts []string) string { diff --git a/vclusterops/helpers_test.go b/vclusterops/helpers_test.go index f3b744b..df8d450 100644 --- a/vclusterops/helpers_test.go +++ b/vclusterops/helpers_test.go @@ -71,6 +71,27 @@ func TestForGetPrimaryHostsWithLatestCatalog(t *testing.T) { assert.Equal(t, primaryHostsWithLatestCatalog, []string{}) } +func TestForGetInitiatorHostInMainCluster(t *testing.T) { + // successfully get an initiator host for subcluster sc1 to promote/demote in the sandbox + mockHostNodeMap := map[string]*VCoordinationNode{ + "192.168.1.101": {Address: "192.168.1.101", State: "UP", Sandbox: "sand", Subcluster: "sc1"}, + "192.168.1.102": {Address: "192.168.1.102", State: "UP", Sandbox: "sand", Subcluster: "sc2"}, + "192.168.1.103": {Address: "192.168.1.103", State: "UP", Sandbox: "", Subcluster: "default_subcluster"}, + "192.168.1.104": {Address: "192.168.1.104", State: "UP", Sandbox: "", Subcluster: "sc4"}} + vdb := VCoordinationDatabase{HostNodeMap: mockHostNodeMap} + initiatorHost, _ := getInitiatorHostInCluster("", "sand", "sc1", &vdb) + assert.Equal(t, initiatorHost, []string{"192.168.1.102"}) + // successfully get an initiator host for default_subcluster to promote/demote in the main subcluster + initiatorHost, _ = getInitiatorHostInCluster("", "", "default_subcluster", &vdb) + assert.Equal(t, initiatorHost, []string{"192.168.1.104"}) + // unable to find any up hosts for default_subcluster in the main subcluster + mockHostNodeMap = map[string]*VCoordinationNode{ + "192.168.1.103": {Address: "192.168.1.103", State: "UP", Sandbox: "", Subcluster: "default_subcluster"}} + vdb = VCoordinationDatabase{HostNodeMap: mockHostNodeMap} + _, err := getInitiatorHostInCluster("", "", "default_subcluster", &vdb) + assert.ErrorContains(t, err, "cannot find any up hosts for subcluster default_subcluster in main subcluster") +} + func TestForgetInitiatorHost(t *testing.T) { nodesList1 := []string{"10.0.0.0", "10.0.0.1", "10.0.0.2"} hostsToSkip1 := []string{"10.0.0.10", "10.0.0.11"} diff --git a/vclusterops/http_adapter.go b/vclusterops/http_adapter.go index b803ae3..9e07ace 100644 --- a/vclusterops/http_adapter.go +++ b/vclusterops/http_adapter.go @@ -19,6 +19,7 @@ import ( "bytes" "crypto/tls" "crypto/x509" + "errors" "fmt" "io" "net/http" @@ -148,7 +149,11 @@ func (adapter *httpAdapter) sendRequest(request *hostHTTPRequest, resultChannel if err != nil { err = fmt.Errorf("fail to send request %v on host %s, details %w", request.Endpoint, adapter.host, err) - resultChannel <- adapter.makeExceptionResult(err) + if errors.Is(err, io.EOF) { + resultChannel <- adapter.makeEOFResult(err) + } else { + resultChannel <- adapter.makeExceptionResult(err) + } return } defer resp.Body.Close() @@ -247,6 +252,16 @@ func (adapter *httpAdapter) makeFailResult(header http.Header, respBody string, } } +// makeEOFResult is a factory method for hostHTTPSResult when an EOF response +// is received from a REST endpoint. +func (adapter *httpAdapter) makeEOFResult(err error) hostHTTPResult { + return hostHTTPResult{ + host: adapter.host, + status: EOF, + err: err, + } +} + // extractErrorFromResponse is called when we get a failed response from a REST // call. We will look at the headers and response body to decide what error // object to create. diff --git a/vclusterops/https_check_db_running_op.go b/vclusterops/https_check_db_running_op.go index 2bde78f..e238bb9 100644 --- a/vclusterops/https_check_db_running_op.go +++ b/vclusterops/https_check_db_running_op.go @@ -30,20 +30,24 @@ type opType int const ( CreateDB opType = iota + DropDB StopDB StartDB ReviveDB StopSC ReIP - checkDBRunningOpName = "HTTPSCheckDBRunningOp" - checkDBRunningOpDesc = "Verify database is running" + checkDBRunningOpName = "HTTPSCheckDBRunningOp" + checkDBRunningOpDesc = "Verify database is running" + checkDBNotRunningOpDesc = "Verify database is not running" ) func (op opType) String() string { switch op { case CreateDB: return "Create DB" + case DropDB: + return "Drop DB" case StopDB: return "Stop DB" case StartDB: @@ -96,6 +100,9 @@ func makeHTTPSCheckRunningDBOp(hosts []string, op.userName = userName op.httpsPassword = httpsPassword op.opType = operationType + if op.opType == StopDB { + op.description = checkDBNotRunningOpDesc + } return op, nil } @@ -108,21 +115,12 @@ func makeHTTPSCheckRunningDBWithSandboxOp(hosts []string, useHTTPPassword bool, userName string, sandbox string, mainCluster bool, httpsPassword *string, operationType opType, ) (httpsCheckRunningDBOp, error) { - op := httpsCheckRunningDBOp{} - op.name = checkDBRunningOpName - op.description = checkDBRunningOpDesc - op.hosts = hosts - op.useHTTPPassword = useHTTPPassword - op.sandbox = sandbox // check if DB is running on specified sandbox - op.mainCluster = mainCluster // check if DB is running on the main cluster - err := util.ValidateUsernameAndPassword(op.name, useHTTPPassword, userName) + op, err := makeHTTPSCheckRunningDBOp(hosts, useHTTPPassword, userName, httpsPassword, operationType) if err != nil { return op, err } - - op.userName = userName - op.httpsPassword = httpsPassword - op.opType = operationType + op.sandbox = sandbox // check if DB is running on specified sandbox + op.mainCluster = mainCluster // check if DB is running on the main cluster return op, nil } @@ -163,6 +161,24 @@ func (op *httpsCheckRunningDBOp) prepare(execContext *opEngineExecContext) error return op.setupClusterHTTPRequest(op.hosts) } +func (op *httpsCheckRunningDBOp) generateHintMessage(host, dbName string) (msg string) { + generalMsg := fmt.Sprintf("[%s] Detected HTTPS service running on host %s", op.name, host) + switch op.opType { + case CreateDB: + msg = fmt.Sprintf("%s, please stop the HTTPS service before creating a new database.", generalMsg) + case DropDB: + msg = fmt.Sprintf("%s, please stop the HTTPS service before dropping the existing database.", generalMsg) + case ReIP: + msg = fmt.Sprintf("%s, please consider using restart_node to re-ip nodes for the running database.", generalMsg) + case StopDB, StartDB, ReviveDB, StopSC: + msg = fmt.Sprintf("%s.", generalMsg) + } + if dbName != "" { + msg += fmt.Sprintf(" Database %s is still running on host %s", dbName, host) + } + return msg +} + /* https v1/nodes endpoint response examples - for a response with 200 status code: @@ -212,41 +228,29 @@ func (op *httpsCheckRunningDBOp) isDBRunningOnHost(host string, runningStatus := "running" startingStatus := "starting/waiting to join cluster" status = runningStatus - // check for rfc error - if !result.isSuccess() && !result.isPassing() { - // hanging HTTPS service thread - switch op.opType { - case CreateDB: - msg = fmt.Sprintf("[%s] Detected HTTPS service running on host %s, please stop the HTTPS service before creating a new database", - op.name, host) - case StopDB, StartDB, ReviveDB, StopSC: - msg = fmt.Sprintf("[%s] Detected HTTPS service running on host %s", op.name, host) - case ReIP: - msg = fmt.Sprintf(`[%s] Detected HTTPS service running on host %s, - please consider using start_node to re-ip nodes for the running database`, + runningDBName := "" + // If request to /nodes is successful, get the dbname for a detailed message + if result.isSuccess() { + nodeList := nodesState.NodeList + if len(nodeList) == 0 { + // exception, throw an error + noNodeErr := fmt.Errorf("[%s] Unexpected result from host %s: empty node_list obtained from /nodes endpoint response", op.name, host) + return status, "", noNodeErr } + nodeInfo := nodeList[0] + runningDBName = nodeInfo.Database + } else { // check whether the node is starting and hasn't pulled the latest catalog yet + // setting status for logging purpose rfcError := &rfc7807.VProblem{} if ok := errors.As(result.err, &rfcError); ok && rfcError.ProblemID == rfc7807.AuthenticationError && strings.Contains(rfcError.Detail, "Local node has not joined cluster yet") { status = startingStatus } - return status, msg, nil - } - nodeList := nodesState.NodeList - if len(nodeList) == 0 { - // exception, throw an error - noNodeErr := fmt.Errorf("[%s] Unexpected result from host %s: empty node_list obtained from /nodes endpoint response", - op.name, host) - return status, "", noNodeErr } - - nodeInfo := nodeList[0] - runningDBName := nodeInfo.Database - - msg = fmt.Sprintf("[%s] Database %s is still running on host %s", op.name, runningDBName, host) + msg = op.generateHintMessage(host, runningDBName) return status, msg, nil } @@ -343,6 +347,10 @@ func (op *httpsCheckRunningDBOp) handleDBRunning(allErrs error, msg string, upHo const createDBMsg = "aborting database creation" op.logger.PrintInfo(createDBMsg) op.updateSpinnerMessage(createDBMsg) + case DropDB: + const dropDBMsg = "aborting database drop" + op.logger.PrintInfo(dropDBMsg) + op.updateSpinnerMessage(dropDBMsg) case StopDB: const stopDBMsg = "the database is not down yet" op.logger.PrintInfo(stopDBMsg) @@ -412,7 +420,7 @@ func (op *httpsCheckRunningDBOp) checkProcessedResult(sandboxedHosts map[string] func (op *httpsCheckRunningDBOp) execute(execContext *opEngineExecContext) error { op.logger.Info("Execute() called", "opType", op.opType) switch op.opType { - case CreateDB, StartDB, ReviveDB, ReIP: + case CreateDB, StartDB, ReviveDB, ReIP, DropDB: return op.checkDBConnection(execContext) case StopDB, StopSC: return op.pollForDBDown(execContext) diff --git a/vclusterops/https_check_node_state_op.go b/vclusterops/https_check_node_state_op.go index 8534841..46adb6a 100644 --- a/vclusterops/https_check_node_state_op.go +++ b/vclusterops/https_check_node_state_op.go @@ -126,6 +126,7 @@ func (op *httpsCheckNodeStateOp) processResult(execContext *opEngineExecContext) } // successful case, write the result into exec context execContext.nodesInfo = nodesInfo.NodeList + op.logger.PrintInfo("reporting results as obtained from the host [%s] ", host) return nil } diff --git a/vclusterops/https_demote_subcluster_op.go b/vclusterops/https_demote_subcluster_op.go new file mode 100644 index 0000000..64f939d --- /dev/null +++ b/vclusterops/https_demote_subcluster_op.go @@ -0,0 +1,131 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "errors" + "fmt" + + "github.com/vertica/vcluster/vclusterops/util" +) + +type httpsDemoteSubclusterOp struct { + opBase + opHTTPSBase + scName string + sandbox string + vdb *VCoordinationDatabase +} + +func makeHTTPSDemoteSubclusterOp(hosts []string, useHTTPPassword bool, + userName string, httpsPassword *string, scName string, sandbox string, + vdb *VCoordinationDatabase) (httpsDemoteSubclusterOp, error) { + op := httpsDemoteSubclusterOp{} + op.name = "HTTPSDemoteSubclusterOp" + op.hosts = hosts + op.description = " Demote a subcluster from primary to secondary" + op.useHTTPPassword = useHTTPPassword + op.scName = scName + op.sandbox = sandbox + op.vdb = vdb + + if useHTTPPassword { + err := util.ValidateUsernameAndPassword(op.name, useHTTPPassword, userName) + if err != nil { + return op, err + } + + op.userName = userName + op.httpsPassword = httpsPassword + } + + return op, nil +} + +func (op *httpsDemoteSubclusterOp) setupClusterHTTPRequest(hosts []string) error { + for _, host := range hosts { + httpRequest := hostHTTPRequest{} + httpRequest.Method = PostMethod + httpRequest.buildHTTPSEndpoint("subclusters/" + op.scName + "/demote") + if op.useHTTPPassword { + httpRequest.Username = op.userName + httpRequest.Password = op.httpsPassword + } + op.clusterHTTPRequest.RequestCollection[host] = httpRequest + } + + return nil +} + +func (op *httpsDemoteSubclusterOp) prepare(execContext *opEngineExecContext) error { + // If no hosts passed in, we will find the hosts from execute-context + if len(op.hosts) == 0 { + upHosts, err := getInitiatorHostInCluster(op.name, op.sandbox, op.scName, op.vdb) + if err != nil { + return fmt.Errorf(`[%s] cannot find initial up hosts in the subcluster %s`, op.name, op.scName) + } + op.hosts = upHosts + } + execContext.dispatcher.setup(op.hosts) + + return op.setupClusterHTTPRequest(op.hosts) +} + +func (op *httpsDemoteSubclusterOp) execute(execContext *opEngineExecContext) error { + if err := op.runExecute(execContext); err != nil { + return err + } + + return op.processResult(execContext) +} + +func (op *httpsDemoteSubclusterOp) processResult(_ *opEngineExecContext) error { + var allErrs error + + for host, result := range op.clusterHTTPRequest.ResultCollection { + op.logResponse(host, result) + + if result.isUnauthorizedRequest() { + // skip checking response from other nodes because we will get the same error there + return result.err + } + if !result.isPassing() { + allErrs = errors.Join(allErrs, result.err) + // try processing other hosts' responses when the current host has some server errors + continue + } + + // decode the json-format response + // The successful response object will be a dictionary: + /* + { + "detail": "DEMOTE SUBCLUSTER TO SECONDARY" + } + */ + _, err := op.parseAndCheckMapResponse(host, result.content) + if err != nil { + return fmt.Errorf(`[%s] fail to parse result on host %s, details: %w`, op.name, host, err) + } + + return nil + } + + return allErrs +} + +func (op *httpsDemoteSubclusterOp) finalize(_ *opEngineExecContext) error { + return nil +} diff --git a/vclusterops/https_get_nodes_info_op.go b/vclusterops/https_get_nodes_info_op.go index 90a892f..6a6f4ca 100644 --- a/vclusterops/https_get_nodes_info_op.go +++ b/vclusterops/https_get_nodes_info_op.go @@ -99,8 +99,9 @@ func (op *httpsGetNodesInfoOp) shouldUseResponse(host string, nodesStates *nodes if responseSandbox != "" && !op.allowUseSandboxResponse { return false } + // continue to parse next response if a response from a different sandbox is expected - if op.sandbox != AnySandbox && responseSandbox != op.sandbox { + if op.sandbox != AnySandbox && responseSandbox != op.sandbox && op.sandbox != util.MainClusterSandbox { return false } return true diff --git a/vclusterops/https_get_up_nodes_op.go b/vclusterops/https_get_up_nodes_op.go index b931b72..cbd056b 100644 --- a/vclusterops/https_get_up_nodes_op.go +++ b/vclusterops/https_get_up_nodes_op.go @@ -33,6 +33,7 @@ const ( StopSubclusterCmd InstallPackageCmd UnsandboxCmd + ManageConnectionDrainingCmd ) type CommandType int @@ -159,21 +160,17 @@ func (op *httpsGetUpNodesOp) processResult(execContext *opEngineExecContext) err op.logResponse(host, result) if !result.isPassing() { allErrs = errors.Join(allErrs, result.err) - } - - // We assume all the hosts are in the same db cluster - // If any of the hosts reject the request, other hosts will reject the request too - // Do not try other hosts when we see a http failure - if result.isFailing() && result.isHTTPRunning() { - exceptionHosts = append(exceptionHosts, host) - continue - } - - if !result.isPassing() { + if result.isUnauthorizedRequest() || result.isInternalError() { + // Authentication error and any unexpected internal server error + exceptionHosts = append(exceptionHosts, host) + continue + } + // Connection refused: node is down downHosts = append(downHosts, host) continue } + // Parse response from /nodes to validate input nodesStates := nodesStateInfo{} err := op.parseAndCheckResponse(host, result.content, &nodesStates) if err != nil { @@ -190,7 +187,7 @@ func (op *httpsGetUpNodesOp) processResult(execContext *opEngineExecContext) err } } - // collect all the up hosts + // Collect all the up hosts err = op.collectUpHosts(nodesStates, host, upHosts, upScInfo, sandboxInfo, upScNodes, scNodes) if err != nil { allErrs = errors.Join(allErrs, err) @@ -208,17 +205,21 @@ func (op *httpsGetUpNodesOp) processResult(execContext *opEngineExecContext) err execContext.nodesInfo = upScNodes.ToSlice() execContext.scNodesInfo = scNodes.ToSlice() execContext.upHostsToSandboxes = sandboxInfo - ignoreErrors := op.processHostLists(upHosts, upScInfo, exceptionHosts, downHosts, sandboxInfo, execContext) + ignoreErrors, errMsg := op.processHostLists(upHosts, upScInfo, exceptionHosts, downHosts, sandboxInfo, execContext) if ignoreErrors { return nil } - - return errors.Join(allErrs, fmt.Errorf("no up nodes detected")) + if errMsg != nil { + return errors.Join(allErrs, errMsg) + } + return allErrs } // Return true if all the results need to be scanned to figure out UP hosts func isCompleteScanRequired(cmdType CommandType) bool { - return cmdType == SandboxCmd || cmdType == StopDBCmd || cmdType == UnsandboxCmd || cmdType == StopSubclusterCmd + return cmdType == SandboxCmd || cmdType == StopDBCmd || + cmdType == UnsandboxCmd || cmdType == StopSubclusterCmd || + cmdType == ManageConnectionDrainingCmd } func (op *httpsGetUpNodesOp) finalize(_ *opEngineExecContext) error { @@ -238,13 +239,13 @@ func (op *httpsGetUpNodesOp) checkSandboxUp(sandboxingInfo map[string]string, sa // down or erratic hosts. Additionally, it determines if the op should fail or not. func (op *httpsGetUpNodesOp) processHostLists(upHosts mapset.Set[string], upScInfo map[string]string, exceptionHosts, downHosts []string, sandboxInfo map[string]string, - execContext *opEngineExecContext) (ignoreErrors bool) { + execContext *opEngineExecContext) (ignoreErrors bool, errMsg error) { execContext.upScInfo = upScInfo // when we found up nodes in the database, but cannot found up nodes in subcluster, we throw an error if op.cmdType == StopSubclusterCmd && upHosts.Cardinality() > 0 && len(execContext.nodesInfo) == 0 { op.logger.PrintError(`[%s] There are no UP nodes in subcluster %s. The subcluster is already down`, op.name, op.scName) - return false + return false, nil } if op.sandbox != "" && op.cmdType != UnsandboxCmd { upSandbox := op.checkSandboxUp(sandboxInfo, op.sandbox) @@ -262,18 +263,20 @@ func (op *httpsGetUpNodesOp) processHostLists(upHosts mapset.Set[string], upScIn execContext.upHosts = upHosts.ToSlice() // sorting the up hosts will be helpful for picking up the initiator in later instructions sort.Strings(execContext.upHosts) - return true + return true, nil } if len(exceptionHosts) > 0 { op.logger.PrintError(`[%s] fail to call https endpoint of database %s on hosts %s`, op.name, op.DBName, exceptionHosts) + errMsg = errors.Join(errMsg, fmt.Errorf("failed to access node on hosts %v", exceptionHosts)) } if len(downHosts) > 0 { op.logger.PrintError(`[%s] did not detect database %s running on hosts %v`, op.name, op.DBName, downHosts) op.updateSpinnerStopFailMessage("did not detect database %s running on hosts %v", op.DBName, downHosts) + errMsg = errors.Join(errMsg, fmt.Errorf("no up node detected on hosts %v", downHosts)) } - return op.noUpHostsOk + return op.noUpHostsOk, errMsg } // validateHosts can validate if hosts in user input matches the ones in GET /nodes response @@ -306,7 +309,6 @@ func (op *httpsGetUpNodesOp) validateHosts(nodesStates nodesStateInfo) error { func (op *httpsGetUpNodesOp) collectUpHosts(nodesStates nodesStateInfo, host string, upHosts mapset.Set[string], upScInfo, sandboxInfo map[string]string, upScNodes, scNodes mapset.Set[NodeInfo]) (err error) { - upMainNodeFound := false foundSC := false for _, node := range nodesStates.NodeList { if node.Database != op.DBName { @@ -319,12 +321,9 @@ func (op *httpsGetUpNodesOp) collectUpHosts(nodesStates nodesStateInfo, host str if node.State == util.NodeUpState { upHosts.Add(node.Address) upScInfo[node.Address] = node.Subcluster - if op.cmdType == StopDBCmd { - if node.Sandbox != util.MainClusterSandbox || !upMainNodeFound { - sandboxInfo[node.Address] = node.Sandbox - // We still need one main cluster UP node, when there are sandboxes - upMainNodeFound = true - } + if op.cmdType == ManageConnectionDrainingCmd || + op.cmdType == StopDBCmd { + sandboxInfo[node.Address] = node.Sandbox } } if op.scName == node.Subcluster { diff --git a/vclusterops/https_promote_subcluster_op.go b/vclusterops/https_promote_subcluster_op.go new file mode 100644 index 0000000..9ca0279 --- /dev/null +++ b/vclusterops/https_promote_subcluster_op.go @@ -0,0 +1,132 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "errors" + "fmt" + + "github.com/vertica/vcluster/vclusterops/util" +) + +type httpsPromoteSubclusterOp struct { + opBase + opHTTPSBase + scName string + sandbox string + vdb *VCoordinationDatabase +} + +func makeHTTPSPromoteSubclusterOp(hosts []string, useHTTPPassword bool, + userName string, httpsPassword *string, scName string, sandbox string, + vdb *VCoordinationDatabase) (httpsPromoteSubclusterOp, error) { + op := httpsPromoteSubclusterOp{} + op.name = "HTTPSPromoteSubclusterOp" + op.description = "Promote a subcluster from secondary to primary" + op.hosts = hosts + op.useHTTPPassword = useHTTPPassword + op.scName = scName + op.sandbox = sandbox + op.vdb = vdb + + if useHTTPPassword { + err := util.ValidateUsernameAndPassword(op.name, useHTTPPassword, userName) + if err != nil { + return op, err + } + + op.userName = userName + op.httpsPassword = httpsPassword + } + + return op, nil +} + +func (op *httpsPromoteSubclusterOp) setupClusterHTTPRequest(hosts []string) error { + for _, host := range hosts { + httpRequest := hostHTTPRequest{} + httpRequest.Method = PostMethod + httpRequest.buildHTTPSEndpoint("subclusters/" + op.scName + "/promote") + if op.useHTTPPassword { + httpRequest.Password = op.httpsPassword + httpRequest.Username = op.userName + } + op.clusterHTTPRequest.RequestCollection[host] = httpRequest + } + + return nil +} + +func (op *httpsPromoteSubclusterOp) prepare(execContext *opEngineExecContext) error { + // If no hosts passed in, we will find the hosts from execute-context + if len(op.hosts) == 0 { + upHosts, err := getInitiatorHostInCluster(op.name, op.sandbox, op.scName, op.vdb) + if err != nil { + return fmt.Errorf(`[%s] cannot find initial up hosts in the subcluster %s`, op.name, op.scName) + } + op.hosts = upHosts + } + + execContext.dispatcher.setup(op.hosts) + + return op.setupClusterHTTPRequest(op.hosts) +} + +func (op *httpsPromoteSubclusterOp) execute(execContext *opEngineExecContext) error { + if err := op.runExecute(execContext); err != nil { + return err + } + + return op.processResult(execContext) +} + +func (op *httpsPromoteSubclusterOp) processResult(_ *opEngineExecContext) error { + var allErrs error + + for host, result := range op.clusterHTTPRequest.ResultCollection { + op.logResponse(host, result) + + if result.isUnauthorizedRequest() { + // skip checking response from other nodes because we will get the same error there + return result.err + } + if !result.isPassing() { + allErrs = errors.Join(allErrs, result.err) + // try processing other hosts' responses when the current host has some server errors + continue + } + + // decode the json-format response + // The successful response object will be a dictionary: + /* + { + "detail": "PROMOTE SUBCLUSTER TO PRIMARY" + } + */ + _, err := op.parseAndCheckMapResponse(host, result.content) + if err != nil { + return fmt.Errorf(`[%s] fail to parse result on host %s, details: %w`, op.name, host, err) + } + + return nil + } + + return allErrs +} + +func (op *httpsPromoteSubclusterOp) finalize(_ *opEngineExecContext) error { + return nil +} diff --git a/vclusterops/https_rename_subcluster_op.go b/vclusterops/https_rename_subcluster_op.go new file mode 100644 index 0000000..e70f08b --- /dev/null +++ b/vclusterops/https_rename_subcluster_op.go @@ -0,0 +1,166 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "errors" + "fmt" + + "github.com/vertica/vcluster/vclusterops/util" +) + +type httpsRenameSubclusterOp struct { + opBase + scName string + newSCName string + sandbox string + vdb *VCoordinationDatabase + opHTTPSBase +} + +func makeHTTPSRenameSubclusterOp(hosts []string, useHTTPPassword bool, + userName string, httpsPassword *string, scName, newSCName, sandbox string, + vdb *VCoordinationDatabase) (httpsRenameSubclusterOp, error) { + op := httpsRenameSubclusterOp{} + op.name = "HTTPSRenameSubclusterOp" + op.description = "Rename a subcluster" + op.hosts = hosts + op.scName = scName + op.newSCName = newSCName + op.sandbox = sandbox + op.vdb = vdb + op.useHTTPPassword = useHTTPPassword + + if useHTTPPassword { + err := util.ValidateUsernameAndPassword(op.name, useHTTPPassword, userName) + if err != nil { + return op, err + } + + op.userName = userName + op.httpsPassword = httpsPassword + } + + return op, nil +} + +func (op *httpsRenameSubclusterOp) setupClusterHTTPRequest(hosts []string) error { + for _, host := range hosts { + httpRequest := hostHTTPRequest{} + httpRequest.Method = PutMethod + httpRequest.buildHTTPSEndpoint("subclusters/" + op.scName + "/rename") + httpRequest.QueryParams = make(map[string]string) + httpRequest.QueryParams["name"] = op.newSCName + + if op.useHTTPPassword { + httpRequest.Password = op.httpsPassword + httpRequest.Username = op.userName + } + op.clusterHTTPRequest.RequestCollection[host] = httpRequest + } + + return nil +} + +func (op *httpsRenameSubclusterOp) prepare(execContext *opEngineExecContext) error { + // If no hosts passed in, we will find the hosts from execute-context + if len(op.hosts) == 0 { + // Find the first up hosts from the main cluster and sandbox + mainUpHost, sandboxUpHost := op.findUpHostForMainAndSandbox(op.vdb) + + // If a sandbox is specified, use two up hosts: one from the main cluster and one from the sandbox + // to execute https put request. Otherwise, use only the up host from the main cluster. + var upHosts []string + if mainUpHost != "" { + upHosts = append(upHosts, mainUpHost) + } else { + return fmt.Errorf(`[%s] cannot find any up host in main cluster`, op.name) + } + if op.sandbox != "" && sandboxUpHost != "" { + upHosts = append(upHosts, sandboxUpHost) + } else if op.sandbox != "" { + return fmt.Errorf(`[%s] cannot find any up host in sandbox %s`, op.name, op.sandbox) + } + op.hosts = upHosts + } + execContext.dispatcher.setup(op.hosts) + + return op.setupClusterHTTPRequest(op.hosts) +} + +func (op *httpsRenameSubclusterOp) findUpHostForMainAndSandbox(vdb *VCoordinationDatabase) (mainUpHost, sandboxUpHost string) { + for _, node := range vdb.HostNodeMap { + if node.State == util.NodeDownState { + continue + } + if node.Sandbox == "" && mainUpHost == "" { + mainUpHost = node.Address + } + if node.Sandbox == op.sandbox && sandboxUpHost == "" && node.Sandbox != "" { + sandboxUpHost = node.Address + } + if mainUpHost != "" && (sandboxUpHost != "" || op.sandbox == "") { + break + } + } + return mainUpHost, sandboxUpHost +} + +func (op *httpsRenameSubclusterOp) execute(execContext *opEngineExecContext) error { + if err := op.runExecute(execContext); err != nil { + return err + } + + return op.processResult(execContext) +} + +func (op *httpsRenameSubclusterOp) processResult(_ *opEngineExecContext) error { + var allErrs error + + for host, result := range op.clusterHTTPRequest.ResultCollection { + op.logResponse(host, result) + + if result.isUnauthorizedRequest() { + // skip checking response from other nodes because we will get the same error there + return result.err + } + if !result.isPassing() { + allErrs = errors.Join(allErrs, result.err) + // try processing other hosts' responses when the current host has some server errors + continue + } + + // decode the json-format response + // The successful response object will be a dictionary: + /* + { + "detail": "" + } + */ + _, err := op.parseAndCheckMapResponse(host, result.content) + if err != nil { + return fmt.Errorf(`[%s] fail to parse result on host %s, details: %w`, op.name, host, err) + } + + return nil + } + + return allErrs +} + +func (op *httpsRenameSubclusterOp) finalize(_ *opEngineExecContext) error { + return nil +} diff --git a/vclusterops/https_stop_db_op.go b/vclusterops/https_stop_db_op.go index d3b25e3..d40a7d9 100644 --- a/vclusterops/https_stop_db_op.go +++ b/vclusterops/https_stop_db_op.go @@ -126,6 +126,11 @@ func (op *httpsStopDBOp) processResult(_ *opEngineExecContext) error { for host, result := range op.clusterHTTPRequest.ResultCollection { op.logResponse(host, result) + // EOF is expected in DB shutdown: we expect the Server HTTPS service to go down quickly + // and the Server HTTPS service does not guarantee that the response being sent back to the client before it closes + if result.isEOF() { + continue + } if !result.isPassing() { allErrs = errors.Join(allErrs, result.err) continue diff --git a/vclusterops/https_stop_node_op.go b/vclusterops/https_stop_node_op.go index d9ad3a5..c68a738 100644 --- a/vclusterops/https_stop_node_op.go +++ b/vclusterops/https_stop_node_op.go @@ -123,6 +123,11 @@ func (op *httpsStopNodeOp) processResult(_ *opEngineExecContext) error { for host, result := range op.clusterHTTPRequest.ResultCollection { op.logResponse(host, result) + // EOF is expected in node shutdown: we expect the node's HTTPS service to go down quickly + // and the Server HTTPS service does not guarantee that the response being sent back to the client before it closes + if result.isEOF() { + continue + } if !result.isPassing() { // If we can't connect to the host, it's already down. That's not an error // Note: We should improve the error handling here. diff --git a/vclusterops/install_packages.go b/vclusterops/install_packages.go index b1f32c2..60902f0 100644 --- a/vclusterops/install_packages.go +++ b/vclusterops/install_packages.go @@ -31,9 +31,18 @@ type VInstallPackagesOptions struct { } func VInstallPackagesOptionsFactory() VInstallPackagesOptions { - opt := VInstallPackagesOptions{} - opt.DatabaseOptions.setDefaultValues() - return opt + options := VInstallPackagesOptions{} + options.DatabaseOptions.setDefaultValues() + return options +} + +func (options *VInstallPackagesOptions) validateParseOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandInstallPackages, logger) + if err != nil { + return err + } + + return nil } // resolve hostnames to be IPs @@ -50,8 +59,8 @@ func (options *VInstallPackagesOptions) analyzeOptions() (err error) { return nil } -func (options *VInstallPackagesOptions) validateAnalyzeOptions(log vlog.Printer) error { - if err := options.validateBaseOptions("install_packages", log); err != nil { +func (options *VInstallPackagesOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } return options.analyzeOptions() diff --git a/vclusterops/manage_connection_draining.go b/vclusterops/manage_connection_draining.go new file mode 100644 index 0000000..3dc7eb8 --- /dev/null +++ b/vclusterops/manage_connection_draining.go @@ -0,0 +1,183 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "fmt" + + "github.com/vertica/vcluster/vclusterops/util" + "github.com/vertica/vcluster/vclusterops/vlog" +) + +const ( + ActionPause ConnectionDrainingAction = "pause" + ActionRedirect ConnectionDrainingAction = "redirect" + ActionResume ConnectionDrainingAction = "resume" +) + +type ConnectionDrainingAction string + +type VManageConnectionDrainingOptions struct { + /* part 1: basic db info */ + DatabaseOptions + + /* part 2: manage connection draining options */ + // the client management action to be performed: pause, redirect, or resume + Action ConnectionDrainingAction + + // the name of the sandbox to target, if left empty the default cluster is assumed + Sandbox string + + // the subcluster to which designated client connection management action will + // be performed, if empty all subclusters will be implied + SCName string + + // the hostname to redirect client connections to, only used when action is redirect + RedirectHostname string +} + +func VManageConnectionDrainingOptionsFactory() VManageConnectionDrainingOptions { + opt := VManageConnectionDrainingOptions{} + // set default values to the params + opt.setDefaultValues() + + return opt +} + +func (opt *VManageConnectionDrainingOptions) validateEonOptions(_ vlog.Printer) error { + if !opt.IsEon { + return fmt.Errorf("manage connections is only supported in Eon mode") + } + return nil +} + +func (opt *VManageConnectionDrainingOptions) validateParseOptions(logger vlog.Printer) error { + err := opt.validateEonOptions(logger) + if err != nil { + return err + } + + err = opt.validateBaseOptions(commandManageConnections, logger) + if err != nil { + return err + } + + return opt.validateExtraOptions(logger) +} + +func (opt *VManageConnectionDrainingOptions) validateExtraOptions(logger vlog.Printer) error { + if opt.Action != ActionPause && + opt.Action != ActionRedirect && + opt.Action != ActionResume { + logger.PrintError("manage connection draining action %q is invalid, must be one of"+ + " %q, %q, or %q", opt.Action, ActionPause, ActionRedirect, ActionResume) + return fmt.Errorf("manage connection draining action %q is invalid", opt.Action) + } + if opt.Action == ActionRedirect { + if opt.RedirectHostname == "" { + logger.PrintError("hostname to redirect to must not be empty"+ + " when manage connection draining action is %q", ActionRedirect) + return fmt.Errorf("hostname to redirect to must not be empty"+ + " when manage connection draining action is %q", ActionRedirect) + } + } + return nil +} + +func (opt *VManageConnectionDrainingOptions) analyzeOptions() (err error) { + // we analyze host names when it is set in user input, otherwise we use hosts in yaml config + if len(opt.RawHosts) > 0 { + // resolve RawHosts to be IP addresses + opt.Hosts, err = util.ResolveRawHostsToAddresses(opt.RawHosts, opt.IPv6) + if err != nil { + return err + } + opt.normalizePaths() + } + return nil +} + +func (opt *VManageConnectionDrainingOptions) validateAnalyzeOptions(log vlog.Printer) error { + if err := opt.validateParseOptions(log); err != nil { + return err + } + err := opt.analyzeOptions() + if err != nil { + return err + } + return opt.setUsePasswordForLocalDBConnection(log) +} + +//nolint:dupl +func (vcc VClusterCommands) VManageConnectionDraining(options *VManageConnectionDrainingOptions) error { + // validate and analyze all options + err := options.validateAnalyzeOptions(vcc.Log) + if err != nil { + return err + } + + // produce manage connection draining instructions + instructions, err := vcc.produceManageConnectionDrainingInstructions(options) + if err != nil { + return fmt.Errorf("fail to produce instructions, %w", err) + } + + // Create a VClusterOpEngine, and add certs to the engine + certs := httpsCerts{key: options.Key, cert: options.Cert, caCert: options.CaCert} + clusterOpEngine := makeClusterOpEngine(instructions, &certs) + + // Give the instructions to the VClusterOpEngine to run + runError := clusterOpEngine.run(vcc.Log) + if runError != nil { + return fmt.Errorf("fail to %v connections: %w", options.Action, runError) + } + + return nil +} + +// The generated instructions will later perform the following operations necessary +// for a successful manage connection draining action. +// - Check NMA connectivity +// - Check UP nodes and sandboxes info +// - Send manage connection draining request +func (vcc VClusterCommands) produceManageConnectionDrainingInstructions( + options *VManageConnectionDrainingOptions) ([]clusterOp, error) { + var instructions []clusterOp + + nmaHealthOp := makeNMAHealthOp(options.Hosts) + + // get up hosts in all sandboxes + httpsGetUpNodesOp, err := makeHTTPSGetUpNodesOp(options.DBName, options.Hosts, + options.usePassword, options.UserName, options.Password, ManageConnectionDrainingCmd) + if err != nil { + return instructions, err + } + + nmaManageConnectionsOp, err := makeNMAManageConnectionsOp(options.Hosts, + options.UserName, options.DBName, options.Sandbox, options.SCName, options.RedirectHostname, + options.Action, options.Password, options.usePassword) + if err != nil { + return instructions, err + } + + instructions = append(instructions, + &nmaHealthOp, + &httpsGetUpNodesOp, + &nmaManageConnectionsOp, + ) + + return instructions, nil +} diff --git a/vclusterops/manage_connection_draining_test.go b/vclusterops/manage_connection_draining_test.go new file mode 100644 index 0000000..016eca9 --- /dev/null +++ b/vclusterops/manage_connection_draining_test.go @@ -0,0 +1,69 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vertica/vcluster/vclusterops/vlog" +) + +func TestVManageConnectionsOptions_validateParseOptions(t *testing.T) { + logger := vlog.Printer{} + + opt := VManageConnectionDrainingOptionsFactory() + testPassword := "test-password" + testSCName := "test-sc" + testDBName := "testdbname" + testUserName := "test-username" + testRedirectHostname := "test-redirect-hostname" + + opt.SCName = testSCName + opt.IsEon = true + opt.RawHosts = append(opt.RawHosts, "test-raw-host") + opt.DBName = testDBName + opt.UserName = testUserName + opt.Password = &testPassword + opt.Action = ActionRedirect + opt.RedirectHostname = testRedirectHostname + + err := opt.validateParseOptions(logger) + assert.NoError(t, err) + + // positive: no username (in which case default OS username will be used) + opt.UserName = "" + err = opt.validateParseOptions(logger) + assert.NoError(t, err) + + // negative: no database name + opt.UserName = testUserName + opt.DBName = "" + err = opt.validateParseOptions(logger) + assert.Error(t, err) + + // negative: no redirect host name when action is redirect + opt.DBName = testDBName + opt.RedirectHostname = "" + err = opt.validateParseOptions(logger) + assert.Error(t, err) + + // negative: wrong action + opt.RedirectHostname = testRedirectHostname + opt.Action = "wrong-action" + err = opt.validateParseOptions(logger) + assert.Error(t, err) +} diff --git a/vclusterops/nma_manage_connections_op.go b/vclusterops/nma_manage_connections_op.go new file mode 100644 index 0000000..35f0a86 --- /dev/null +++ b/vclusterops/nma_manage_connections_op.go @@ -0,0 +1,152 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/vertica/vcluster/vclusterops/util" +) + +type nmaManageConnectionsOp struct { + opBase + hostRequestBody string + sandbox string + action ConnectionDrainingAction + initiator string +} + +type sqlEndpointData struct { + DBUsername string `json:"username"` + DBPassword string `json:"password"` + DBName string `json:"dbname"` +} + +type manageConnectionsData struct { + sqlEndpointData + SubclusterName string `json:"subclustername"` + RedirectHostname string `json:"hostname"` +} + +func makeNMAManageConnectionsOp(hosts []string, + username, dbName, sandbox, subclusterName, redirectHostname string, action ConnectionDrainingAction, + password *string, useHTTPPassword bool) (nmaManageConnectionsOp, error) { + op := nmaManageConnectionsOp{} + op.name = "NMAManageConnectionsOp" + op.description = "Manage connections on Vertica hosts" + op.hosts = hosts + op.action = action + op.sandbox = sandbox + + err := op.setupRequestBody(username, dbName, subclusterName, redirectHostname, password, + useHTTPPassword) + if err != nil { + return op, err + } + + return op, nil +} + +func createSQLEndpointData(username, dbName string, useDBPassword bool, password *string) sqlEndpointData { + sqlConnectionData := sqlEndpointData{} + sqlConnectionData.DBUsername = username + sqlConnectionData.DBName = dbName + if useDBPassword { + sqlConnectionData.DBPassword = *password + } + return sqlConnectionData +} + +func (op *nmaManageConnectionsOp) setupRequestBody( + username, dbName, subclusterName, redirectHostname string, password *string, + useDBPassword bool) error { + err := util.ValidateSQLEndpointData(op.name, + useDBPassword, username, password, dbName) + if err != nil { + return err + } + manageConnData := manageConnectionsData{} + manageConnData.sqlEndpointData = createSQLEndpointData(username, dbName, useDBPassword, password) + manageConnData.SubclusterName = subclusterName + if op.action == ActionRedirect { + manageConnData.RedirectHostname = redirectHostname + } + + dataBytes, err := json.Marshal(manageConnData) + if err != nil { + return fmt.Errorf("[%s] fail to marshal request data to JSON string, detail %w", op.name, err) + } + + op.hostRequestBody = string(dataBytes) + + op.logger.Info("request data", "op name", op.name, "hostRequestBody", op.hostRequestBody) + + return nil +} + +func (op *nmaManageConnectionsOp) setupClusterHTTPRequest(initiator string, action ConnectionDrainingAction) error { + httpRequest := hostHTTPRequest{} + httpRequest.Method = PostMethod + httpRequest.buildNMAEndpoint("connections/" + string(action)) + httpRequest.RequestData = op.hostRequestBody + op.clusterHTTPRequest.RequestCollection[initiator] = httpRequest + + return nil +} + +func (op *nmaManageConnectionsOp) prepare(execContext *opEngineExecContext) error { + // select an up host in the sandbox as the initiator + initiator, err := getInitiatorInSandbox(op.sandbox, op.hosts, execContext.upHostsToSandboxes) + if err != nil { + return err + } + op.initiator = initiator + execContext.dispatcher.setup([]string{op.initiator}) + return op.setupClusterHTTPRequest(op.initiator, op.action) +} + +func (op *nmaManageConnectionsOp) execute(execContext *opEngineExecContext) error { + if err := op.runExecute(execContext); err != nil { + return err + } + + return op.processResult(execContext) +} + +func (op *nmaManageConnectionsOp) finalize(_ *opEngineExecContext) error { + return nil +} + +func (op *nmaManageConnectionsOp) processResult(_ *opEngineExecContext) error { + var allErrs error + + for host, result := range op.clusterHTTPRequest.ResultCollection { + op.logResponse(host, result) + + if result.isPassing() { + _, err := op.parseAndCheckMapResponse(host, result.content) + if err != nil { + allErrs = errors.Join(allErrs, err) + } + } else { + allErrs = errors.Join(allErrs, result.err) + } + } + + return allErrs +} diff --git a/vclusterops/nma_manage_connections_op_test.go b/vclusterops/nma_manage_connections_op_test.go new file mode 100644 index 0000000..06049d3 --- /dev/null +++ b/vclusterops/nma_manage_connections_op_test.go @@ -0,0 +1,58 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNmaManageConnectionsOp_SetupRequestBody(t *testing.T) { + op := &nmaManageConnectionsOp{} + op.action = ActionRedirect + + username := "test-user" + dbName := "test-db" + subclusterName := "test-subcluster" + redirectHostname := "test-redirect" + password := "test-password-op" + useDBPassword := true + + err := op.setupRequestBody(username, dbName, subclusterName, redirectHostname, &password, useDBPassword) + assert.NoError(t, err) + + expectedData := manageConnectionsData{ + SubclusterName: subclusterName, + RedirectHostname: redirectHostname, + sqlEndpointData: createSQLEndpointData(username, dbName, useDBPassword, &password), + } + + expectedBytes, _ := json.Marshal(expectedData) + expectedRequestBody := string(expectedBytes) + + assert.Equal(t, expectedRequestBody, op.hostRequestBody) + + err = op.setupRequestBody("", dbName, subclusterName, redirectHostname, &password, useDBPassword) + assert.Error(t, err) + + err = op.setupRequestBody(username, "", subclusterName, redirectHostname, &password, useDBPassword) + assert.Error(t, err) + + err = op.setupRequestBody(username, dbName, subclusterName, redirectHostname, nil, useDBPassword) + assert.Error(t, err) +} diff --git a/vclusterops/nma_read_catalog_editor_op.go b/vclusterops/nma_read_catalog_editor_op.go index 9d94af4..9ed2f18 100644 --- a/vclusterops/nma_read_catalog_editor_op.go +++ b/vclusterops/nma_read_catalog_editor_op.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + "github.com/vertica/vcluster/rfc7807" "golang.org/x/exp/maps" ) @@ -28,6 +29,8 @@ type nmaReadCatalogEditorOp struct { initiator []string // used when creating new nodes vdb *VCoordinationDatabase catalogPathMap map[string]string + + firstStartAfterRevive bool // used for start_db only } // makeNMAReadCatalogEditorOpWithInitiator creates an op to read catalog editor info. @@ -49,6 +52,18 @@ func makeNMAReadCatalogEditorOp(vdb *VCoordinationDatabase) (nmaReadCatalogEdito return makeNMAReadCatalogEditorOpWithInitiator([]string{}, vdb) } +func makeNMAReadCatalogEditorOpForStartDB( + vdb *VCoordinationDatabase, + firstStartAfterRevive bool) (nmaReadCatalogEditorOp, error) { + op, err := makeNMAReadCatalogEditorOpWithInitiator([]string{}, vdb) + if err != nil { + return op, err + } + + op.firstStartAfterRevive = firstStartAfterRevive + return op, err +} + func (op *nmaReadCatalogEditorOp) setupClusterHTTPRequest(hosts []string) error { for _, host := range hosts { httpRequest := hostHTTPRequest{} @@ -169,6 +184,7 @@ func (op *nmaReadCatalogEditorOp) processResult(execContext *opEngineExecContext var hostsWithLatestCatalog []string var maxGlobalVersion int64 var latestNmaVDB nmaVDatabase + var bestHost string for host, result := range op.clusterHTTPRequest.ResultCollection { op.logResponse(host, result) @@ -208,10 +224,24 @@ func (op *nmaReadCatalogEditorOp) processResult(execContext *opEngineExecContext maxGlobalVersion = globalVersion // save the latest NMAVDatabase to execContext latestNmaVDB = nmaVDB + bestHost = host } else if globalVersion == maxGlobalVersion { hostsWithLatestCatalog = append(hostsWithLatestCatalog, host) } } else { + // if this is not the first time of start_db after revive_db, + // we ignore the error if the catalog directory is empty, because + // - we may send request to a secondary node right after revive + // - users may delete the catalog files + if !op.firstStartAfterRevive { + rfcError := &rfc7807.VProblem{} + if ok := errors.As(result.err, &rfcError); ok && + (rfcError.ProblemID == rfc7807.CECatalogContentDirEmptyError || + rfcError.ProblemID == rfc7807.CECatalogContentDirNotExistError) { + continue + } + } + allErrs = errors.Join(allErrs, result.err) } } @@ -226,6 +256,6 @@ func (op *nmaReadCatalogEditorOp) processResult(execContext *opEngineExecContext execContext.hostsWithLatestCatalog = hostsWithLatestCatalog // save the latest nmaVDB to execContext execContext.nmaVDatabase = latestNmaVDB - + op.logger.PrintInfo("reporting results as obtained from the host [%s] ", bestHost) return allErrs } diff --git a/vclusterops/nma_stage_commands_op.go b/vclusterops/nma_stage_commands_op.go index 99e0fa5..0c9110e 100644 --- a/vclusterops/nma_stage_commands_op.go +++ b/vclusterops/nma_stage_commands_op.go @@ -24,10 +24,13 @@ import ( type nmaStageCommandsOp struct { scrutinizeOpBase + skipCollectLibs bool } type stageCommandsRequestData struct { - CatalogPath string `json:"catalog_path"` + CatalogPath string `json:"catalog_path"` + SkipCollectLibs bool `json:"skip_collect_libs,omitempty"` + IncludeCatalogLibs bool `json:"include_catalog_libs,omitempty"` } type stageCommandsResponseData struct { @@ -37,8 +40,8 @@ type stageCommandsResponseData struct { func makeNMAStageCommandsOp(logger vlog.Printer, id, batch string, hosts []string, - hostNodeNameMap map[string]string, - hostCatPathMap map[string]string) (nmaStageCommandsOp, error) { + hostNodeNameMap, hostCatPathMap map[string]string, + skipCollectLibs bool) (nmaStageCommandsOp, error) { // base members op := nmaStageCommandsOp{} op.name = "NMAStageCommandsOp" @@ -54,6 +57,9 @@ func makeNMAStageCommandsOp(logger vlog.Printer, op.httpMethod = PostMethod op.urlSuffix = "/commands" + // custom members + op.skipCollectLibs = skipCollectLibs + // the caller is responsible for making sure hosts and maps match up exactly err := validateHostMaps(hosts, hostNodeNameMap, hostCatPathMap) return op, err @@ -61,9 +67,14 @@ func makeNMAStageCommandsOp(logger vlog.Printer, func (op *nmaStageCommandsOp) setupRequestBody(hosts []string) error { op.hostRequestBodyMap = make(map[string]string, len(hosts)) - for _, host := range hosts { + for i, host := range hosts { stageCommandsData := stageCommandsRequestData{} stageCommandsData.CatalogPath = op.hostCatPathMap[host] + stageCommandsData.SkipCollectLibs = op.skipCollectLibs + if i == 0 { + // on one host, we collect all .so files in the catalog/Libraries directory + stageCommandsData.IncludeCatalogLibs = true + } dataBytes, err := json.Marshal(stageCommandsData) if err != nil { diff --git a/vclusterops/nma_vertica_version_op.go b/vclusterops/nma_vertica_version_op.go index b42baf2..539bd6a 100644 --- a/vclusterops/nma_vertica_version_op.go +++ b/vclusterops/nma_vertica_version_op.go @@ -165,7 +165,7 @@ func (op *nmaVerticaVersionOp) prepare(execContext *opEngineExecContext) error { op.hosts = append(op.hosts, host) sc := vnode.Subcluster // Update subcluster of new nodes that will be assigned to default subcluster. - // When we created vdb in db_add_node without specifying subcluster, we did not know the default subcluster name + // When we created vdb in add_node without specifying subcluster, we did not know the default subcluster name // so new nodes is using "" as their subclusters. Below line will correct node nodes' subclusters. if op.vdb.IsEon && sc == "" && execContext.defaultSCName != "" { op.vdb.HostNodeMap[host].Subcluster = execContext.defaultSCName diff --git a/vclusterops/re_ip.go b/vclusterops/re_ip.go index 2650da1..73325f5 100644 --- a/vclusterops/re_ip.go +++ b/vclusterops/re_ip.go @@ -41,55 +41,78 @@ type VReIPOptions struct { } func VReIPFactory() VReIPOptions { - opt := VReIPOptions{} + options := VReIPOptions{} // set default values to the params - opt.setDefaultValues() - opt.TrimReIPList = false + options.setDefaultValues() + options.TrimReIPList = false - return opt + return options } -func (opt *VReIPOptions) validateParseOptions(logger vlog.Printer) error { - err := util.ValidateRequiredAbsPath(opt.CatalogPrefix, "catalog path") +func (options *VReIPOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandReIP, logger) + if err != nil { + return err + } + return nil +} + +func (options *VReIPOptions) validateExtraOptions() error { + err := util.ValidateRequiredAbsPath(options.CatalogPrefix, "catalog path") if err != nil { return err } - if opt.CommunalStorageLocation != "" { - return util.ValidateCommunalStorageLocation(opt.CommunalStorageLocation) + if options.CommunalStorageLocation != "" { + return util.ValidateCommunalStorageLocation(options.CommunalStorageLocation) } - return opt.validateBaseOptions("re_ip", logger) + return nil } -func (opt *VReIPOptions) analyzeOptions() error { - if len(opt.RawHosts) > 0 { - hostAddresses, err := util.ResolveRawHostsToAddresses(opt.RawHosts, opt.IPv6) +func (options *VReIPOptions) validateParseOptions(logger vlog.Printer) error { + // batch 1: validate required parameters + err := options.validateRequiredOptions(logger) + if err != nil { + return err + } + + // batch 2: validate all other params + err = options.validateExtraOptions() + if err != nil { + return err + } + return nil +} + +func (options *VReIPOptions) analyzeOptions() error { + if len(options.RawHosts) > 0 { + hostAddresses, err := util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - opt.Hosts = hostAddresses + options.Hosts = hostAddresses } return nil } -func (opt *VReIPOptions) validateAnalyzeOptions(logger vlog.Printer) error { - if err := opt.validateParseOptions(logger); err != nil { +func (options *VReIPOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } - if err := opt.analyzeOptions(); err != nil { + if err := options.analyzeOptions(); err != nil { return err } // the re-ip list must not be empty - if len(opt.ReIPList) == 0 { + if len(options.ReIPList) == 0 { return errors.New("the re-ip list is not provided") } // address check - ipv6 := opt.IPv6 + ipv6 := options.IPv6 nodeAddresses := make(map[string]struct{}) - for _, info := range opt.ReIPList { + for _, info := range options.ReIPList { // the addresses must be valid IPs if err := util.AddressCheck(info.TargetAddress, ipv6); err != nil { return err @@ -261,7 +284,7 @@ type reIPRow struct { // ReadReIPFile reads the re-IP file and builds a slice of ReIPInfo. // It returns any error encountered. -func (opt *VReIPOptions) ReadReIPFile(path string) error { +func (options *VReIPOptions) ReadReIPFile(path string) error { if err := util.AbsPathCheck(path); err != nil { return fmt.Errorf("must specify an absolute path for the re-ip file") } @@ -295,7 +318,7 @@ func (opt *VReIPOptions) ReadReIPFile(path string) error { return nil } - ipv6 := opt.IPv6 + ipv6 := options.IPv6 for _, row := range reIPRows { var info ReIPInfo info.NodeAddress = row.CurrentAddress @@ -307,7 +330,7 @@ func (opt *VReIPOptions) ReadReIPFile(path string) error { info.TargetControlAddress = row.NewControlAddress info.TargetControlBroadcast = row.NewControlBroadcast - opt.ReIPList = append(opt.ReIPList, info) + options.ReIPList = append(options.ReIPList, info) } return nil diff --git a/vclusterops/remove_node.go b/vclusterops/remove_node.go index e61f9e7..acd0422 100644 --- a/vclusterops/remove_node.go +++ b/vclusterops/remove_node.go @@ -34,73 +34,77 @@ type VRemoveNodeOptions struct { } func VRemoveNodeOptionsFactory() VRemoveNodeOptions { - opt := VRemoveNodeOptions{} + options := VRemoveNodeOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } -func (o *VRemoveNodeOptions) setDefaultValues() { - o.DatabaseOptions.setDefaultValues() +func (options *VRemoveNodeOptions) setDefaultValues() { + options.DatabaseOptions.setDefaultValues() - o.ForceDelete = true - o.IsSubcluster = false + options.ForceDelete = true + options.IsSubcluster = false } -func (o *VRemoveNodeOptions) validateRequiredOptions(log vlog.Printer) error { - err := o.validateBaseOptions("db_remove_node", log) +func (options *VRemoveNodeOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandRemoveNode, logger) if err != nil { return err } return nil } -func (o *VRemoveNodeOptions) validateExtraOptions() error { +func (options *VRemoveNodeOptions) validateExtraOptions() error { // data prefix - if o.DataPrefix != "" { - return util.ValidateRequiredAbsPath(o.DataPrefix, "data path") + if options.DataPrefix != "" { + return util.ValidateRequiredAbsPath(options.DataPrefix, "data path") } return nil } -func (o *VRemoveNodeOptions) validateParseOptions(log vlog.Printer) error { +func (options *VRemoveNodeOptions) validateParseOptions(logger vlog.Printer) error { // batch 1: validate required params - err := o.validateRequiredOptions(log) + err := options.validateRequiredOptions(logger) if err != nil { return err } // batch 2: validate all other params - return o.validateExtraOptions() + err = options.validateExtraOptions() + if err != nil { + return err + } + return nil } -func (o *VRemoveNodeOptions) analyzeOptions() (err error) { - o.HostsToRemove, err = util.ResolveRawHostsToAddresses(o.HostsToRemove, o.IPv6) +func (options *VRemoveNodeOptions) analyzeOptions() (err error) { + options.HostsToRemove, err = util.ResolveRawHostsToAddresses(options.HostsToRemove, options.IPv6) if err != nil { return err } // we analyze host names when it is set in user input, otherwise we use hosts in yaml config - if len(o.RawHosts) > 0 { + if len(options.RawHosts) > 0 { // resolve RawHosts to be IP addresses - o.Hosts, err = util.ResolveRawHostsToAddresses(o.RawHosts, o.IPv6) + options.Hosts, err = util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - o.normalizePaths() + options.normalizePaths() } return nil } -func (o *VRemoveNodeOptions) validateAnalyzeOptions(log vlog.Printer) error { - if err := o.validateParseOptions(log); err != nil { +func (options *VRemoveNodeOptions) validateAnalyzeOptions(log vlog.Printer) error { + if err := options.validateParseOptions(log); err != nil { return err } - err := o.analyzeOptions() + err := options.analyzeOptions() if err != nil { return err } - return o.setUsePassword(log) + return options.setUsePassword(log) } func (vcc VClusterCommands) VRemoveNode(options *VRemoveNodeOptions) (VCoordinationDatabase, error) { @@ -253,27 +257,27 @@ func checkRemoveNodeRequirements(vdb *VCoordinationDatabase, options *VRemoveNod // completeVDBSetting sets some VCoordinationDatabase fields we cannot get yet // from the https endpoints. We set those fields from options. -func (o *VRemoveNodeOptions) completeVDBSetting(vdb *VCoordinationDatabase) error { - vdb.DataPrefix = o.DataPrefix +func (options *VRemoveNodeOptions) completeVDBSetting(vdb *VCoordinationDatabase) error { + vdb.DataPrefix = options.DataPrefix - if o.DepotPrefix == "" { + if options.DepotPrefix == "" { return nil } if vdb.IsEon { // checking this here because now we have got eon value from // the running db. This will be removed once we are able to get // the depot path from db through an https endpoint(VER-88122). - err := util.ValidateRequiredAbsPath(o.DepotPrefix, "depot path") + err := util.ValidateRequiredAbsPath(options.DepotPrefix, "depot path") if err != nil { return err } } - vdb.DepotPrefix = o.DepotPrefix + vdb.DepotPrefix = options.DepotPrefix hostNodeMap := makeVHostNodeMap() // TODO: we set the depot path from /nodes rather than manually // (VER-92725). This is useful for nmaDeleteDirectoriesOp. for h, vnode := range vdb.HostNodeMap { - vnode.DepotPath = vdb.genDepotPath(vnode.Name) + vnode.DepotPath = vdb.GenDepotPath(vnode.Name) hostNodeMap[h] = vnode } vdb.HostNodeMap = hostNodeMap @@ -495,12 +499,12 @@ func (vcc VClusterCommands) produceSpreadRemoveNodeOp(instructions *[]clusterOp, // setInitiator sets the initiator as the first primary up node that is not // in the list of hosts to remove. -func (o *VRemoveNodeOptions) setInitiator(primaryUpNodes []string) error { - initiatorHost, err := getInitiatorHost(primaryUpNodes, o.HostsToRemove) +func (options *VRemoveNodeOptions) setInitiator(primaryUpNodes []string) error { + initiatorHost, err := getInitiatorHost(primaryUpNodes, options.HostsToRemove) if err != nil { return err } - o.Initiator = initiatorHost + options.Initiator = initiatorHost return nil } diff --git a/vclusterops/remove_subcluster.go b/vclusterops/remove_subcluster.go index c8f7109..5543253 100644 --- a/vclusterops/remove_subcluster.go +++ b/vclusterops/remove_subcluster.go @@ -28,91 +28,103 @@ import ( // database. type VRemoveScOptions struct { DatabaseOptions - SubclusterToRemove string // subcluster to remove from database - ForceDelete bool // whether force delete directories + SCName string // subcluster to remove from database + ForceDelete bool // whether force delete directories } func VRemoveScOptionsFactory() VRemoveScOptions { - opt := VRemoveScOptions{} + options := VRemoveScOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } -func (o *VRemoveScOptions) setDefaultValues() { - o.DatabaseOptions.setDefaultValues() +func (options *VRemoveScOptions) setDefaultValues() { + options.DatabaseOptions.setDefaultValues() } -func (o *VRemoveScOptions) validateRequiredOptions(logger vlog.Printer) error { - err := o.validateBaseOptions("db_remove_subcluster", logger) +func (options *VRemoveScOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandRemoveSubcluster, logger) if err != nil { return err } - if o.SubclusterToRemove == "" { + if options.SCName == "" { return fmt.Errorf("must specify a subcluster name") } + + err = util.ValidateScName(options.SCName) + if err != nil { + return err + } return nil } -func (o *VRemoveScOptions) validatePathOptions() error { +func (options *VRemoveScOptions) validateEonOptions() error { + if !options.IsEon { + return fmt.Errorf(`cannot remove subcluster from an enterprise database '%s'`, + options.DBName) + } + return nil +} + +func (options *VRemoveScOptions) validateExtraOptions() error { // VER-88096 will get data path and depot path from /nodes // so the validation below may be removed // data prefix - err := util.ValidateRequiredAbsPath(o.DataPrefix, "data path") + err := util.ValidateRequiredAbsPath(options.DataPrefix, "data path") if err != nil { return err } // depot path - return util.ValidateRequiredAbsPath(o.DepotPrefix, "depot path") + return util.ValidateRequiredAbsPath(options.DepotPrefix, "depot path") } -func (o *VRemoveScOptions) validateParseOptions(logger vlog.Printer) error { - err := o.validateRequiredOptions(logger) +func (options *VRemoveScOptions) validateParseOptions(logger vlog.Printer) error { + // batch 1: validate required params + err := options.validateRequiredOptions(logger) if err != nil { return err } - err = o.validateEonOptions() + // batch 2: validate eon params + err = options.validateEonOptions() if err != nil { return err } - return o.validatePathOptions() -} - -func (o *VRemoveScOptions) validateEonOptions() error { - if !o.IsEon { - return fmt.Errorf(`cannot remove subcluster from an enterprise database '%s'`, - o.DBName) + // batch 3: validate all other params + err = options.validateExtraOptions() + if err != nil { + return err } return nil } -func (o *VRemoveScOptions) analyzeOptions() (err error) { +func (options *VRemoveScOptions) analyzeOptions() (err error) { // we analyze host names when it is set in user input, otherwise we use hosts in yaml config - if len(o.RawHosts) > 0 { + if len(options.RawHosts) > 0 { // resolve RawHosts to be IP addresses - o.Hosts, err = util.ResolveRawHostsToAddresses(o.RawHosts, o.IPv6) + options.Hosts, err = util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - o.normalizePaths() + options.normalizePaths() } return nil } -func (o *VRemoveScOptions) validateAnalyzeOptions(logger vlog.Printer) error { - if err := o.validateParseOptions(logger); err != nil { +func (options *VRemoveScOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } - err := o.analyzeOptions() + err := options.analyzeOptions() if err != nil { return err } - return o.setUsePassword(logger) + return options.setUsePassword(logger) } // VRemoveSubcluster removes a subcluster. It returns updated database catalog information and any error encountered. @@ -130,19 +142,19 @@ func (vcc VClusterCommands) VRemoveSubcluster(removeScOpt *VRemoveScOptions) (VC } // pre-check: should not remove the default subcluster - vcc.PrintInfo("Performing db_remove_subcluster pre-checks") + vcc.PrintInfo("Performing remove_subcluster pre-checks") hostsToRemove, err := vcc.removeScPreCheck(&vdb, removeScOpt) if err != nil { return vdb, err } - // proceed to run db_remove_node only if + // proceed to run remove_node only if // the number of nodes to remove is greater than zero var needRemoveNodes bool vcc.Log.V(1).Info("Nodes to be removed: %+v", hostsToRemove) if len(hostsToRemove) == 0 { vcc.Log.PrintInfo("no node found in subcluster %s", - removeScOpt.SubclusterToRemove) + removeScOpt.SCName) needRemoveNodes = false } else { needRemoveNodes = true @@ -157,7 +169,7 @@ func (vcc VClusterCommands) VRemoveSubcluster(removeScOpt *VRemoveScOptions) (VC removeNodeOpt.IsSubcluster = true vcc.Log.PrintInfo("Removing nodes %q from subcluster %s", - hostsToRemove, removeScOpt.SubclusterToRemove) + hostsToRemove, removeScOpt.SCName) vdb, err = vcc.VRemoveNode(&removeNodeOpt) if err != nil { return vdb, err @@ -183,7 +195,7 @@ func (e *removeDefaultSubclusterError) Error() string { } // removeScPreCheck will build a list of instructions to perform -// db_remove_subcluster pre-checks +// remove_subcluster pre-checks // // The generated instructions will later perform the following operations necessary // for a successful remove_node: @@ -191,7 +203,7 @@ func (e *removeDefaultSubclusterError) Error() string { // - Get the subcluster info (check if the target sc exists and if it is the default sc) func (vcc VClusterCommands) removeScPreCheck(vdb *VCoordinationDatabase, options *VRemoveScOptions) ([]string, error) { var hostsToRemove []string - const preCheckErrMsg = "while performing db_remove_subcluster pre-checks" + const preCheckErrMsg = "while performing remove_subcluster pre-checks" // get cluster and nodes info err := vcc.getVDBFromRunningDB(vdb, &options.DatabaseOptions) @@ -199,7 +211,7 @@ func (vcc VClusterCommands) removeScPreCheck(vdb *VCoordinationDatabase, options return hostsToRemove, err } - // db_remove_subcluster only works with Eon database + // remove_subcluster only works with Eon database if !vdb.IsEon { // info from running db confirms that the db is not Eon return hostsToRemove, fmt.Errorf(`cannot remove subcluster from an enterprise database '%s'`, @@ -215,7 +227,7 @@ func (vcc VClusterCommands) removeScPreCheck(vdb *VCoordinationDatabase, options // cannot remove sandbox subcluster httpsFindSubclusterOp, err := makeHTTPSFindSubclusterOp(options.Hosts, options.usePassword, options.UserName, options.Password, - options.SubclusterToRemove, + options.SCName, false /*do not ignore not found*/, RemoveSubclusterCmd) if err != nil { return hostsToRemove, fmt.Errorf("fail to get default subcluster %s, details: %w", @@ -241,13 +253,13 @@ func (vcc VClusterCommands) removeScPreCheck(vdb *VCoordinationDatabase, options } // the default subcluster should not be removed - if options.SubclusterToRemove == clusterOpEngine.execContext.defaultSCName { - return hostsToRemove, &removeDefaultSubclusterError{Name: options.SubclusterToRemove} + if options.SCName == clusterOpEngine.execContext.defaultSCName { + return hostsToRemove, &removeDefaultSubclusterError{Name: options.SCName} } // get nodes of the to-be-removed subcluster for h, vnode := range vdb.HostNodeMap { - if vnode.Subcluster == options.SubclusterToRemove { + if vnode.Subcluster == options.SCName { hostsToRemove = append(hostsToRemove, h) } } @@ -257,15 +269,15 @@ func (vcc VClusterCommands) removeScPreCheck(vdb *VCoordinationDatabase, options // completeVDBSetting sets some VCoordinationDatabase fields we cannot get yet // from the https endpoints. We set those fields from options. -func (o *VRemoveScOptions) completeVDBSetting(vdb *VCoordinationDatabase) error { - vdb.DataPrefix = o.DataPrefix - vdb.DepotPrefix = o.DepotPrefix +func (options *VRemoveScOptions) completeVDBSetting(vdb *VCoordinationDatabase) error { + vdb.DataPrefix = options.DataPrefix + vdb.DepotPrefix = options.DepotPrefix hostNodeMap := makeVHostNodeMap() // TODO: we set the depot path from /nodes rather than manually // (VER-92725). This is useful for nmaDeleteDirectoriesOp. for h, vnode := range vdb.HostNodeMap { - vnode.DepotPath = vdb.genDepotPath(vnode.Name) + vnode.DepotPath = vdb.GenDepotPath(vnode.Name) hostNodeMap[h] = vnode } vdb.HostNodeMap = hostNodeMap @@ -273,7 +285,7 @@ func (o *VRemoveScOptions) completeVDBSetting(vdb *VCoordinationDatabase) error } func (vcc VClusterCommands) dropSubcluster(vdb *VCoordinationDatabase, options *VRemoveScOptions) error { - dropScErrMsg := fmt.Sprintf("fail to drop subcluster %s", options.SubclusterToRemove) + dropScErrMsg := fmt.Sprintf("fail to drop subcluster %s", options.SCName) // the initiator is a list of one primary up host // that will call the https /v1/subclusters/{scName}/drop endpoint @@ -284,7 +296,7 @@ func (vcc VClusterCommands) dropSubcluster(vdb *VCoordinationDatabase, options * } httpsDropScOp, err := makeHTTPSDropSubclusterOp([]string{initiator}, - options.SubclusterToRemove, + options.SCName, options.usePassword, options.UserName, options.Password) if err != nil { vcc.Log.Error(err, "details: %v", dropScErrMsg) diff --git a/vclusterops/remove_subcluster_test.go b/vclusterops/remove_subcluster_test.go index 4db2554..b2956f0 100644 --- a/vclusterops/remove_subcluster_test.go +++ b/vclusterops/remove_subcluster_test.go @@ -36,7 +36,7 @@ func TestRemoveSubcluster(t *testing.T) { assert.ErrorContains(t, err, "must specify a subcluster name") // input sc name - options.SubclusterToRemove = "sc1" + options.SCName = "sc1" // verify Eon mode is set options.IsEon = false diff --git a/vclusterops/rename_subcluster.go b/vclusterops/rename_subcluster.go new file mode 100644 index 0000000..db61299 --- /dev/null +++ b/vclusterops/rename_subcluster.go @@ -0,0 +1,164 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "fmt" + + "github.com/vertica/vcluster/vclusterops/util" + "github.com/vertica/vcluster/vclusterops/vlog" +) + +type VRenameSubclusterOptions struct { + // Basic db info + DatabaseOptions + // Name of the subcluster to rename + SCName string + // New name of the subcluster + NewSCName string + // Name of the sandbox + // Use this option when renaming a subcluster in a sandbox. + // if this option is not set, the subcluster will be renamed in the main cluster. + Sandbox string +} + +func VRenameSubclusterFactory() VRenameSubclusterOptions { + options := VRenameSubclusterOptions{} + // set default values to the params + options.setDefaultValues() + return options +} + +func (options *VRenameSubclusterOptions) validateEonOptions(_ vlog.Printer) error { + if !options.IsEon { + return fmt.Errorf("rename subcluster is only supported in Eon mode") + } + return nil +} + +func (options *VRenameSubclusterOptions) validateParseOptions(logger vlog.Printer) error { + err := options.validateEonOptions(logger) + if err != nil { + return err + } + + // need to provide a password or certs + if options.Password == nil && (options.Cert == "" || options.Key == "") { + return fmt.Errorf("must provide a password or certs") + } + + if options.SCName == "" { + return fmt.Errorf("must specify a subcluster name") + } + + err = util.ValidateScName(options.SCName) + if err != nil { + return err + } + + if options.NewSCName == "" { + return fmt.Errorf("must specify a new subcluster name") + } + + err = util.ValidateScName(options.NewSCName) + if err != nil { + return err + } + return options.validateBaseOptions(commandRenameSc, logger) +} + +// analyzeOptions will modify some options based on what is chosen +func (options *VRenameSubclusterOptions) analyzeOptions() (err error) { + // we analyze host names when it is set in user input, otherwise we use hosts in yaml config + if len(options.RawHosts) > 0 { + // resolve RawHosts to be IP addresses + options.Hosts, err = util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) + if err != nil { + return err + } + } + return nil +} + +func (options *VRenameSubclusterOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { + return err + } + return options.analyzeOptions() +} + +// VRenameSubcluster alter the name of the specified subcluster +func (vcc VClusterCommands) VRenameSubcluster(options *VRenameSubclusterOptions) error { + /* + * - Produce Instructions + * - Create a VClusterOpEngine + * - Give the instructions to the VClusterOpEngine to run + */ + + // validate and analyze options + err := options.validateAnalyzeOptions(vcc.Log) + if err != nil { + return err + } + + // retrieve information from the database to accurately determine the state of each node in both the main cluster and sandbox + vdb := makeVCoordinationDatabase() + err = vcc.getVDBFromRunningDB(&vdb, &options.DatabaseOptions) + if err != nil { + return err + } + + // produce rename subcluster instructions + instructions, err := vcc.produceRenameSubclusterInstructions(options, &vdb) + if err != nil { + return fmt.Errorf("fail to produce instructions, %w", err) + } + + // create a VClusterOpEngine, and add certs to the engine + certs := httpsCerts{key: options.Key, cert: options.Cert, caCert: options.CaCert} + clusterOpEngine := makeClusterOpEngine(instructions, &certs) + + // give the instructions to the VClusterOpEngine to run + runError := clusterOpEngine.run(vcc.Log) + if runError != nil { + return fmt.Errorf("fail to rename subcluster: %w", runError) + } + + return nil +} + +// The generated instructions will later perform the following operations necessary +// for a successful promote/demote subcluster operation: +// - Rename subclusters using one of the up nodes +func (vcc VClusterCommands) produceRenameSubclusterInstructions(options *VRenameSubclusterOptions, + vdb *VCoordinationDatabase) ([]clusterOp, error) { + var instructions []clusterOp + + // need username for https operations + err := options.setUsePassword(vcc.Log) + if err != nil { + return instructions, err + } + + var noHosts = []string{} // We pass in no hosts so that this op picks an up node from the previous call. + httpsRenameScOp, err := makeHTTPSRenameSubclusterOp(noHosts, options.usePassword, + options.UserName, options.Password, options.SCName, options.NewSCName, options.Sandbox, vdb) + if err != nil { + return nil, err + } + instructions = append(instructions, &httpsRenameScOp) + return instructions, nil +} diff --git a/vclusterops/rename_subcluster_test.go b/vclusterops/rename_subcluster_test.go new file mode 100644 index 0000000..32f7940 --- /dev/null +++ b/vclusterops/rename_subcluster_test.go @@ -0,0 +1,75 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vertica/vcluster/vclusterops/vlog" +) + +const ( + testSCName = "testsc" + testDBName = "testdbname" + testUserName = "test-username" +) + +func TestVRenameSubclusterOptions_validateParseOptions(t *testing.T) { + logger := vlog.Printer{} + + opt := VRenameSubclusterFactory() + + opt.IsEon = true + opt.SCName = testSCName + opt.NewSCName = testSCName + opt.RawHosts = append(opt.RawHosts, "test-raw-host") + opt.DBName = testDBName + opt.UserName = testUserName + testPassword := "test-password-2" + opt.Password = &testPassword + + err := opt.validateParseOptions(logger) + assert.NoError(t, err) + + opt.UserName = "" + err = opt.validateParseOptions(logger) + assert.NoError(t, err) + + // negative: no subcluster name + opt.DBName = testDBName + opt.SCName = "" + err = opt.validateParseOptions(logger) + assert.ErrorContains(t, err, "must specify a subcluster name") + + // negative: no new subcluster name + opt.SCName = testSCName + opt.NewSCName = "" + err = opt.validateParseOptions(logger) + assert.ErrorContains(t, err, "must specify a new subcluster name") + + // negative: no database name + opt.UserName = testUserName + opt.NewSCName = testSCName + opt.DBName = "" + err = opt.validateParseOptions(logger) + assert.ErrorContains(t, err, "must specify a database name") + + // negative: enterprise database + opt.IsEon = false + err = opt.validateParseOptions(logger) + assert.ErrorContains(t, err, "rename subcluster is only supported in Eon mode") +} diff --git a/vclusterops/replication.go b/vclusterops/replication.go index 6283471..a12168e 100644 --- a/vclusterops/replication.go +++ b/vclusterops/replication.go @@ -33,82 +33,114 @@ type VReplicationDatabaseOptions struct { TargetUserName string TargetPassword *string SourceTLSConfig string - Sandbox string + SandboxName string } func VReplicationDatabaseFactory() VReplicationDatabaseOptions { - opt := VReplicationDatabaseOptions{} + options := VReplicationDatabaseOptions{} // set default values to the params - opt.setDefaultValues() - return opt + options.setDefaultValues() + return options } -func (opt *VReplicationDatabaseOptions) validateEonOptions(_ vlog.Printer) error { - if !opt.IsEon { - return fmt.Errorf("replication is only supported in Eon mode") +func (options *VReplicationDatabaseOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandReplicationStart, logger) + if err != nil { + return err } return nil } -func (opt *VReplicationDatabaseOptions) validateParseOptions(logger vlog.Printer) error { - err := opt.validateEonOptions(logger) - if err != nil { - return err +func (options *VReplicationDatabaseOptions) validateEonOptions() error { + if !options.IsEon { + return fmt.Errorf("replication is only supported in Eon mode") } - if len(opt.TargetHosts) == 0 { + return nil +} + +func (options *VReplicationDatabaseOptions) validateExtraOptions() error { + if len(options.TargetHosts) == 0 { return fmt.Errorf("must specify a target host or target host list") } // valiadate target database - if opt.TargetDB == "" { + if options.TargetDB == "" { return fmt.Errorf("must specify a target database name") } - err = util.ValidateDBName(opt.TargetDB) + err := util.ValidateDBName(options.TargetDB) if err != nil { return err } // need to provide a password or certs in source database - if opt.Password == nil && (opt.Cert == "" || opt.Key == "") { + if options.Password == nil && (options.Cert == "" || options.Key == "") { return fmt.Errorf("must provide a password or certs") } // need to provide a password or TLSconfig if source and target username are different - if opt.TargetUserName != opt.UserName { - if opt.TargetPassword == nil && opt.SourceTLSConfig == "" { + if options.TargetUserName != options.UserName { + if options.TargetPassword == nil && options.SourceTLSConfig == "" { return fmt.Errorf("only trust authentication can support username without password or TLSConfig") } } - return opt.validateBaseOptions(commandReplicationStart, logger) + if options.SandboxName != "" { + err = util.ValidateSandboxName(options.SandboxName) + if err != nil { + return err + } + } + + return nil +} + +func (options *VReplicationDatabaseOptions) validateParseOptions(logger vlog.Printer) error { + // batch 1: validate required parameters + err := options.validateRequiredOptions(logger) + if err != nil { + return err + } + + // batch 2: validate eon params + err = options.validateEonOptions() + if err != nil { + return err + } + + // batch 3: validate all other params + err = options.validateExtraOptions() + if err != nil { + return err + } + return nil } // analyzeOptions will modify some options based on what is chosen -func (opt *VReplicationDatabaseOptions) analyzeOptions() (err error) { - if len(opt.TargetHosts) > 0 { +func (options *VReplicationDatabaseOptions) analyzeOptions() (err error) { + if len(options.TargetHosts) > 0 { // resolve RawHosts to be IP addresses - opt.TargetHosts, err = util.ResolveRawHostsToAddresses(opt.TargetHosts, opt.IPv6) + options.TargetHosts, err = util.ResolveRawHostsToAddresses(options.TargetHosts, options.IPv6) if err != nil { return err } } // we analyze host names when it is set in user input, otherwise we use hosts in yaml config - if len(opt.RawHosts) > 0 { + if len(options.RawHosts) > 0 { // resolve RawHosts to be IP addresses - hostAddresses, err := util.ResolveRawHostsToAddresses(opt.RawHosts, opt.IPv6) + hostAddresses, err := util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - opt.Hosts = hostAddresses + options.Hosts = hostAddresses } return nil } -func (opt *VReplicationDatabaseOptions) validateAnalyzeOptions(logger vlog.Printer) error { - if err := opt.validateParseOptions(logger); err != nil { +func (options *VReplicationDatabaseOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } - return opt.analyzeOptions() + return options.analyzeOptions() } // VReplicateDatabase can copy all table data and metadata from this cluster to another @@ -192,7 +224,7 @@ func (vcc VClusterCommands) produceDBReplicationInstructions(options *VReplicati initiatorTargetHost := getInitiator(options.TargetHosts) httpsStartReplicationOp, err := makeHTTPSStartReplicationOp(options.DBName, options.Hosts, options.usePassword, options.UserName, options.Password, targetUsePassword, options.TargetDB, options.TargetUserName, initiatorTargetHost, - options.TargetPassword, options.SourceTLSConfig, options.Sandbox) + options.TargetPassword, options.SourceTLSConfig, options.SandboxName) if err != nil { return instructions, err } diff --git a/vclusterops/restore_points.go b/vclusterops/restore_points.go index 84ab170..780fdcf 100644 --- a/vclusterops/restore_points.go +++ b/vclusterops/restore_points.go @@ -31,29 +31,29 @@ type VShowRestorePointsOptions struct { } func VShowRestorePointsFactory() VShowRestorePointsOptions { - opt := VShowRestorePointsOptions{} + options := VShowRestorePointsOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - opt.FilterOptions = ShowRestorePointFilterOptions{} + options.FilterOptions = ShowRestorePointFilterOptions{} - return opt + return options } -func (p *ShowRestorePointFilterOptions) hasNonEmptyStartTimestamp() bool { - return (p.StartTimestamp != "") +func (options *ShowRestorePointFilterOptions) hasNonEmptyStartTimestamp() bool { + return (options.StartTimestamp != "") } -func (p *ShowRestorePointFilterOptions) hasNonEmptyEndTimestamp() bool { - return (p.EndTimestamp != "") +func (options *ShowRestorePointFilterOptions) hasNonEmptyEndTimestamp() bool { + return (options.EndTimestamp != "") } // Check that all non-empty timestamps specified have valid date time or date only format, // convert date only format to date time format when applicable, and make sure end timestamp // is no earlier than start timestamp -func (p *ShowRestorePointFilterOptions) ValidateAndStandardizeTimestampsIfAny() (err error) { +func (options *ShowRestorePointFilterOptions) ValidateAndStandardizeTimestampsIfAny() (err error) { // shortcut of no validation needed - if !p.hasNonEmptyStartTimestamp() && !p.hasNonEmptyEndTimestamp() { + if !options.hasNonEmptyStartTimestamp() && !options.hasNonEmptyEndTimestamp() { return nil } @@ -61,36 +61,36 @@ func (p *ShowRestorePointFilterOptions) ValidateAndStandardizeTimestampsIfAny() var dateTimeErr, dateOnlyErr error // try date time first - parsedStartDatetime, dateTimeErr := util.IsEmptyOrValidTimeStr(util.DefaultDateTimeFormat, p.StartTimestamp) + parsedStartDatetime, dateTimeErr := util.IsEmptyOrValidTimeStr(util.DefaultDateTimeFormat, options.StartTimestamp) if dateTimeErr != nil { // fallback to date only - parsedStartDatetime, dateOnlyErr = util.IsEmptyOrValidTimeStr(util.DefaultDateOnlyFormat, p.StartTimestamp) + parsedStartDatetime, dateOnlyErr = util.IsEmptyOrValidTimeStr(util.DefaultDateOnlyFormat, options.StartTimestamp) if dateOnlyErr != nil { // give up return fmt.Errorf("start timestamp %q is invalid; cannot parse as a datetime: %w; "+ - "cannot parse as a date as well: %w", p.StartTimestamp, dateTimeErr, dateOnlyErr) + "cannot parse as a date as well: %w", options.StartTimestamp, dateTimeErr, dateOnlyErr) } // default value of time parsed from date only string is already indicating the start of a day - // invoke this function here to only rewrite p.StartTimestamp in date time format - util.FillInDefaultTimeForStartTimestamp(&p.StartTimestamp) + // invoke this function here to only rewrite options.StartTimestamp in date time format + util.FillInDefaultTimeForStartTimestamp(&options.StartTimestamp) } // try date time first - parsedEndDatetime, dateTimeErr := util.IsEmptyOrValidTimeStr(util.DefaultDateTimeFormat, p.EndTimestamp) + parsedEndDatetime, dateTimeErr := util.IsEmptyOrValidTimeStr(util.DefaultDateTimeFormat, options.EndTimestamp) if dateTimeErr != nil { // fallback to date only - _, dateOnlyErr = util.IsEmptyOrValidTimeStr(util.DefaultDateOnlyFormat, p.EndTimestamp) + _, dateOnlyErr = util.IsEmptyOrValidTimeStr(util.DefaultDateOnlyFormat, options.EndTimestamp) if dateOnlyErr != nil { // give up return fmt.Errorf("end timestamp %q is invalid; cannot parse as a datetime: %w; "+ - "cannot parse as a date as well: %w", p.EndTimestamp, dateTimeErr, dateOnlyErr) + "cannot parse as a date as well: %w", options.EndTimestamp, dateTimeErr, dateOnlyErr) } // fill in default value for time and update the end timestamp - parsedEndDatetime = util.FillInDefaultTimeForEndTimestamp(&p.EndTimestamp) + parsedEndDatetime = util.FillInDefaultTimeForEndTimestamp(&options.EndTimestamp) } // check if endTime is after start time if both of them are non-empty - if p.hasNonEmptyStartTimestamp() && p.hasNonEmptyEndTimestamp() { + if options.hasNonEmptyStartTimestamp() && options.hasNonEmptyEndTimestamp() { validRange := util.IsTimeEqualOrAfter(*parsedStartDatetime, *parsedEndDatetime) if !validRange { return errors.New("start timestamp must be before end timestamp") @@ -101,44 +101,62 @@ func (p *ShowRestorePointFilterOptions) ValidateAndStandardizeTimestampsIfAny() return nil } -func (opt *VShowRestorePointsOptions) validateParseOptions(logger vlog.Printer) error { - err := opt.validateBaseOptions("show_restore_points", logger) +func (options *VShowRestorePointsOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandShowRestorePoints, logger) if err != nil { return err } - err = util.ValidateCommunalStorageLocation(opt.CommunalStorageLocation) + err = util.ValidateCommunalStorageLocation(options.CommunalStorageLocation) + if err != nil { + return err + } + return nil +} + +func (options *VShowRestorePointsOptions) validateExtraOptions() error { + err := options.FilterOptions.ValidateAndStandardizeTimestampsIfAny() if err != nil { return err } - err = opt.FilterOptions.ValidateAndStandardizeTimestampsIfAny() + return nil +} + +func (options *VShowRestorePointsOptions) validateParseOptions(logger vlog.Printer) error { + // batch 1: validate required parameters + err := options.validateRequiredOptions(logger) if err != nil { return err } + // batch 2: validate all other params + err = options.validateExtraOptions() + if err != nil { + return err + } return nil } // analyzeOptions will modify some options based on what is chosen -func (opt *VShowRestorePointsOptions) analyzeOptions() (err error) { +func (options *VShowRestorePointsOptions) analyzeOptions() (err error) { // we analyze host names when it is set in user input, otherwise we use hosts in yaml config - if len(opt.RawHosts) > 0 { + if len(options.RawHosts) > 0 { // resolve RawHosts to be IP addresses - hostAddresses, err := util.ResolveRawHostsToAddresses(opt.RawHosts, opt.IPv6) + hostAddresses, err := util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - opt.Hosts = hostAddresses + options.Hosts = hostAddresses } return nil } -func (opt *VShowRestorePointsOptions) validateAnalyzeOptions(logger vlog.Printer) error { - if err := opt.validateParseOptions(logger); err != nil { +func (options *VShowRestorePointsOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } - return opt.analyzeOptions() + return options.analyzeOptions() } // VShowRestorePoints can query the restore points from an archive diff --git a/vclusterops/revive_db.go b/vclusterops/revive_db.go index 1f5edd2..54df725 100644 --- a/vclusterops/revive_db.go +++ b/vclusterops/revive_db.go @@ -109,12 +109,12 @@ func (e *ReviveDBRestorePointNotFoundError) Error() string { } func VReviveDBOptionsFactory() VReviveDatabaseOptions { - opt := VReviveDatabaseOptions{} + options := VReviveDatabaseOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } func (options *VReviveDatabaseOptions) setDefaultValues() { @@ -129,7 +129,7 @@ func (options *VReviveDatabaseOptions) validateRequiredOptions() error { if options.DBName == "" { return fmt.Errorf("must specify a database name") } - err := util.ValidateName(options.DBName, "database") + err := util.ValidateDBName(options.DBName) if err != nil { return err } @@ -144,7 +144,7 @@ func (options *VReviveDatabaseOptions) validateRequiredOptions() error { return util.ValidateCommunalStorageLocation(options.CommunalStorageLocation) } -func (options *VReviveDatabaseOptions) validateRestoreOptions() error { +func (options *VReviveDatabaseOptions) validateExtraOptions() error { if options.isRestoreEnabled() && options.hasValidRestorePointID() == options.hasValidRestorePointIndex() { return fmt.Errorf("for a restore, must specify exactly one of (1-based) restore point index or id, " + @@ -155,12 +155,18 @@ func (options *VReviveDatabaseOptions) validateRestoreOptions() error { } func (options *VReviveDatabaseOptions) validateParseOptions() error { + // batch 1: validate required parameters err := options.validateRequiredOptions() if err != nil { return err } - return options.validateRestoreOptions() + // batch 2: validate all other params + err = options.validateExtraOptions() + if err != nil { + return err + } + return nil } // analyzeOptions will modify some options based on what is chosen diff --git a/vclusterops/sandbox.go b/vclusterops/sandbox.go index adc7739..10003dc 100644 --- a/vclusterops/sandbox.go +++ b/vclusterops/sandbox.go @@ -31,9 +31,9 @@ type VSandboxOptions struct { } func VSandboxOptionsFactory() VSandboxOptions { - opt := VSandboxOptions{} - opt.setDefaultValues() - return opt + options := VSandboxOptions{} + options.setDefaultValues() + return options } func (options *VSandboxOptions) setDefaultValues() { @@ -41,7 +41,7 @@ func (options *VSandboxOptions) setDefaultValues() { } func (options *VSandboxOptions) validateRequiredOptions(logger vlog.Printer) error { - err := options.validateBaseOptions("sandbox_subcluster", logger) + err := options.validateBaseOptions(commandSandboxSC, logger) if err != nil { return err } @@ -50,9 +50,28 @@ func (options *VSandboxOptions) validateRequiredOptions(logger vlog.Printer) err return fmt.Errorf("must specify a subcluster name") } + err = util.ValidateScName(options.SCName) + if err != nil { + return err + } + if options.SandboxName == "" { return fmt.Errorf("must specify a sandbox name") } + + err = util.ValidateSandboxName(options.SandboxName) + if err != nil { + return err + } + return nil +} + +func (options *VSandboxOptions) validateParseOptions(logger vlog.Printer) error { + // batch 1: validate required parameters + err := options.validateRequiredOptions(logger) + if err != nil { + return err + } return nil } @@ -78,8 +97,9 @@ func (options *VSandboxOptions) analyzeOptions() (err error) { return nil } -func (options *VSandboxOptions) ValidateAnalyzeOptions(vcc VClusterCommands) error { - if err := options.validateRequiredOptions(vcc.Log); err != nil { +func (options *VSandboxOptions) ValidateAnalyzeOptions(logger vlog.Printer) error { + err := options.validateParseOptions(logger) + if err != nil { return err } return options.analyzeOptions() @@ -158,7 +178,7 @@ func (vcc VClusterCommands) VSandbox(options *VSandboxOptions) error { // sandboxInterface is an interface that will be used by runSandboxCmd(). // The purpose of this interface is to avoid code duplication. type sandboxInterface interface { - ValidateAnalyzeOptions(vcc VClusterCommands) error + ValidateAnalyzeOptions(logger vlog.Printer) error runCommand(vcc VClusterCommands) error } @@ -186,7 +206,7 @@ func (options *VSandboxOptions) runCommand(vcc VClusterCommands) error { // It can avoid code duplication between VSandbox and VUnsandbox. func runSandboxCmd(vcc VClusterCommands, i sandboxInterface) error { // check required options - err := i.ValidateAnalyzeOptions(vcc) + err := i.ValidateAnalyzeOptions(vcc.Log) if err != nil { vcc.Log.Error(err, "failed to validate the options") return err diff --git a/vclusterops/scrutinize.go b/vclusterops/scrutinize.go index 50c9ac6..0bde889 100644 --- a/vclusterops/scrutinize.go +++ b/vclusterops/scrutinize.go @@ -54,6 +54,7 @@ type VScrutinizeOptions struct { IncludeRos bool IncludeExternalTableDetails bool IncludeUDXDetails bool + SkipCollectLibs bool LogAgeOldestTime string LogAgeNewestTime string LogAgeHours int // max log age from input @@ -64,9 +65,9 @@ type VScrutinizeOptions struct { } func VScrutinizeOptionsFactory() VScrutinizeOptions { - opt := VScrutinizeOptions{} - opt.setDefaultValues() - return opt + options := VScrutinizeOptions{} + options.setDefaultValues() + return options } // human description of the scrutinize formats for archived log time range parameters @@ -154,7 +155,12 @@ func (options *VScrutinizeOptions) validateRequiredOptions(logger vlog.Printer) } func (options *VScrutinizeOptions) validateParseOptions(logger vlog.Printer) error { - return options.validateRequiredOptions(logger) + // batch 1: validate required parameters + err := options.validateRequiredOptions(logger) + if err != nil { + return err + } + return nil } // analyzeOptions will modify some options based on what is chosen @@ -376,7 +382,7 @@ func (vcc VClusterCommands) produceScrutinizeInstructions(options *VScrutinizeOp // run and stage diagnostic command results -- see NMA for what commands are run stageCommandsOp, err := makeNMAStageCommandsOp(vcc.Log, options.ID, scrutinizeBatchContext, - options.Hosts, hostNodeNameMap, hostCatPathMap) + options.Hosts, hostNodeNameMap, hostCatPathMap, options.SkipCollectLibs) if err != nil { return nil, err } diff --git a/vclusterops/start_db.go b/vclusterops/start_db.go index 349ba05..4156ca3 100644 --- a/vclusterops/start_db.go +++ b/vclusterops/start_db.go @@ -26,6 +26,12 @@ import ( // with VStartDatabase. type VStartDatabaseOptions struct { // basic db info + // Devloper Guide: + // The --hosts flag for start_db is used to start sandboxed hosts + // If you want to partial start a down db by using --hosts + // You should input more than half of the primary nodes to meet the quorum requirement + // If quorum requirement is not meet, the start_db process will hang until timeout + // And you need to manually kill those startup failed vertica processes. DatabaseOptions // timeout for polling the states of all nodes in the database in HTTPSPollNodeStateOp StatePollingTimeout int @@ -38,15 +44,18 @@ type VStartDatabaseOptions struct { StartUpConf string // whether the provided hosts are in a sandbox HostsInSandbox bool + + // whether the first time to start the database after revive + FirstStartAfterRevive bool } func VStartDatabaseOptionsFactory() VStartDatabaseOptions { - opt := VStartDatabaseOptions{} + options := VStartDatabaseOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } func (options *VStartDatabaseOptions) setDefaultValues() { @@ -56,11 +65,10 @@ func (options *VStartDatabaseOptions) setDefaultValues() { } func (options *VStartDatabaseOptions) validateRequiredOptions(logger vlog.Printer) error { - err := options.validateBaseOptions("start_db", logger) + err := options.validateBaseOptions(commandStartDB, logger) if err != nil { return err } - return options.validateCatalogPath() } @@ -68,7 +76,6 @@ func (options *VStartDatabaseOptions) validateEonOptions() error { if options.CommunalStorageLocation != "" { return util.ValidateCommunalStorageLocation(options.CommunalStorageLocation) } - return nil } @@ -79,7 +86,11 @@ func (options *VStartDatabaseOptions) validateParseOptions(logger vlog.Printer) return err } // batch 2: validate eon params - return options.validateEonOptions() + err = options.validateEonOptions() + if err != nil { + return err + } + return nil } func (options *VStartDatabaseOptions) analyzeOptions() (err error) { @@ -257,7 +268,7 @@ func (vcc VClusterCommands) produceStartDBPreCheck(options *VStartDatabaseOption // find latest catalog to use for removal of nodes not in the catalog if trimHostList { - nmaReadCatalogEditorOp, err := makeNMAReadCatalogEditorOp(vdb) + nmaReadCatalogEditorOp, err := makeNMAReadCatalogEditorOpForStartDB(vdb, options.FirstStartAfterRevive) if err != nil { return instructions, err } @@ -282,7 +293,7 @@ func (vcc VClusterCommands) produceStartDBInstructions(options *VStartDatabaseOp var instructions []clusterOp // vdb here should contain only primary nodes - nmaReadCatalogEditorOp, err := makeNMAReadCatalogEditorOp(vdb) + nmaReadCatalogEditorOp, err := makeNMAReadCatalogEditorOpForStartDB(vdb, options.FirstStartAfterRevive) if err != nil { return instructions, err } diff --git a/vclusterops/start_node.go b/vclusterops/start_node.go index 57fddee..b7dde2e 100644 --- a/vclusterops/start_node.go +++ b/vclusterops/start_node.go @@ -58,11 +58,11 @@ type VStartNodesInfo struct { } func VStartNodesOptionsFactory() VStartNodesOptions { - opt := VStartNodesOptions{} + options := VStartNodesOptions{} // set default values to the params - opt.setDefaultValues() - return opt + options.setDefaultValues() + return options } func (options *VStartNodesOptions) setDefaultValues() { @@ -72,8 +72,21 @@ func (options *VStartNodesOptions) setDefaultValues() { options.Nodes = make(map[string]string) } +func (options *VStartNodesOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandRestartNode, logger) + if err != nil { + return err + } + return nil +} + func (options *VStartNodesOptions) validateParseOptions(logger vlog.Printer) error { - return options.validateBaseOptions("restart_node", logger) + // batch 1: validate required parameters + err := options.validateRequiredOptions(logger) + if err != nil { + return err + } + return nil } // analyzeOptions will modify some options based on what is chosen diff --git a/vclusterops/start_subcluster.go b/vclusterops/start_subcluster.go index bc43fb0..8bc81e7 100644 --- a/vclusterops/start_subcluster.go +++ b/vclusterops/start_subcluster.go @@ -28,73 +28,84 @@ import ( type VStartScOptions struct { DatabaseOptions VStartNodesOptions - SubclusterToStart string // subcluster to start + SCName string // subcluster to start } func VStartScOptionsFactory() VStartScOptions { - opt := VStartScOptions{} + options := VStartScOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } -func (o *VStartScOptions) setDefaultValues() { - o.DatabaseOptions.setDefaultValues() - o.VStartNodesOptions.setDefaultValues() +func (options *VStartScOptions) setDefaultValues() { + options.DatabaseOptions.setDefaultValues() + options.VStartNodesOptions.setDefaultValues() } -func (o *VStartScOptions) validateRequiredOptions(logger vlog.Printer) error { - err := o.validateBaseOptions("start_subcluster", logger) +func (options *VStartScOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandStartSubcluster, logger) if err != nil { return err } - if o.SubclusterToStart == "" { + if options.SCName == "" { return fmt.Errorf("must specify a subcluster name") } + + err = util.ValidateScName(options.SCName) + if err != nil { + return err + } return nil } -func (o *VStartScOptions) validateEonOptions() error { - if !o.IsEon { +func (options *VStartScOptions) validateEonOptions() error { + if !options.IsEon { return fmt.Errorf(`cannot start subcluster from an enterprise database '%s'`, - o.DBName) + options.DBName) } return nil } -func (o *VStartScOptions) validateParseOptions(logger vlog.Printer) error { - err := o.validateRequiredOptions(logger) +func (options *VStartScOptions) validateParseOptions(logger vlog.Printer) error { + // batch 1: validate required parameters + err := options.validateRequiredOptions(logger) if err != nil { return err } - return o.validateEonOptions() + // batch 2: validate eon params + err = options.validateEonOptions() + if err != nil { + return err + } + return nil } -func (o *VStartScOptions) analyzeOptions() (err error) { +func (options *VStartScOptions) analyzeOptions() (err error) { // we analyze host names when it is set in user input, otherwise we use hosts in yaml config - if len(o.RawHosts) > 0 { + if len(options.RawHosts) > 0 { // resolve RawHosts to be IP addresses - o.Hosts, err = util.ResolveRawHostsToAddresses(o.RawHosts, o.IPv6) + options.Hosts, err = util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - o.normalizePaths() + options.normalizePaths() } return nil } -func (o *VStartScOptions) validateAnalyzeOptions(logger vlog.Printer) error { - if err := o.validateParseOptions(logger); err != nil { +func (options *VStartScOptions) validateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } - err := o.analyzeOptions() + err := options.analyzeOptions() if err != nil { return err } - return o.setUsePassword(logger) + return options.setUsePassword(logger) } // VStartSubcluster start nodes in a subcluster. It returns any error encountered. @@ -119,14 +130,14 @@ func (vcc VClusterCommands) VStartSubcluster(options *VStartScOptions) error { // collect down nodes to start in the target subcluster for _, vnode := range vdb.HostNodeMap { - if vnode.Subcluster == options.SubclusterToStart && vnode.State == util.NodeDownState { + if vnode.Subcluster == options.SCName && vnode.State == util.NodeDownState { nodesToStart[vnode.Name] = vnode.Address } } if len(nodesToStart) == 0 { return fmt.Errorf("cannot find down node to start in subcluster %s", - options.SubclusterToStart) + options.SCName) } var startNodesOptions VStartNodesOptions @@ -135,6 +146,7 @@ func (vcc VClusterCommands) VStartSubcluster(options *VStartScOptions) error { startNodesOptions.StatePollingTimeout = options.StatePollingTimeout startNodesOptions.vdb = &vdb - fmt.Printf("Starting nodes %v in subcluster %s\n", maps.Keys(nodesToStart), options.SubclusterToStart) + vlog.DisplayColorInfo("Starting nodes %v in subcluster %s", maps.Keys(nodesToStart), options.SCName) + return vcc.VStartNodes(&startNodesOptions) } diff --git a/vclusterops/stop_db.go b/vclusterops/stop_db.go index e0f1ea9..c3f5763 100644 --- a/vclusterops/stop_db.go +++ b/vclusterops/stop_db.go @@ -28,7 +28,7 @@ type VStopDatabaseOptions struct { /* part 2: eon db info */ DrainSeconds *int // time in seconds to wait for database users' disconnection - Sandbox string // Stop db on given sandbox + SandboxName string // Stop db on given sandbox MainCluster bool // Stop db on main cluster only /* part 3: hidden info */ CheckUserConn bool // whether check user connection @@ -36,11 +36,11 @@ type VStopDatabaseOptions struct { } func VStopDatabaseOptionsFactory() VStopDatabaseOptions { - opt := VStopDatabaseOptions{} + options := VStopDatabaseOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } func (options *VStopDatabaseOptions) setDefaultValues() { @@ -48,7 +48,7 @@ func (options *VStopDatabaseOptions) setDefaultValues() { } func (options *VStopDatabaseOptions) validateRequiredOptions(log vlog.Printer) error { - err := options.validateBaseOptions("stop_db", log) + err := options.validateBaseOptions(commandStopDB, log) if err != nil { return err } @@ -57,7 +57,7 @@ func (options *VStopDatabaseOptions) validateRequiredOptions(log vlog.Printer) e } func (options *VStopDatabaseOptions) validateEonOptions(log vlog.Printer) error { - if options.Sandbox != "" && options.MainCluster { + if options.SandboxName != "" && options.MainCluster { return fmt.Errorf("Error: cannot use both --sandbox and --main-cluster-only options together ") } @@ -77,6 +77,12 @@ func (options *VStopDatabaseOptions) validateEonOptions(log vlog.Printer) error } func (options *VStopDatabaseOptions) validateExtraOptions() error { + if options.SandboxName != "" { + err := util.ValidateSandboxName(options.SandboxName) + if err != nil { + return err + } + } return nil } @@ -186,7 +192,7 @@ func (vcc *VClusterCommands) produceStopDBInstructions(options *VStopDatabaseOpt } httpsGetUpNodesOp, err := makeHTTPSGetUpNodesWithSandboxOp(options.DBName, options.Hosts, - usePassword, options.UserName, options.Password, StopDBCmd, options.Sandbox, options.MainCluster) + usePassword, options.UserName, options.Password, StopDBCmd, options.SandboxName, options.MainCluster) if err != nil { return instructions, err } @@ -203,13 +209,13 @@ func (vcc *VClusterCommands) produceStopDBInstructions(options *VStopDatabaseOpt } httpsStopDBOp, err := makeHTTPSStopDBOp(usePassword, options.UserName, options.Password, options.DrainSeconds, - options.Sandbox, options.MainCluster) + options.SandboxName, options.MainCluster) if err != nil { return instructions, err } httpsCheckDBRunningOp, err := makeHTTPSCheckRunningDBWithSandboxOp(options.Hosts, - usePassword, options.UserName, options.Sandbox, options.MainCluster, options.Password, StopDB) + usePassword, options.UserName, options.SandboxName, options.MainCluster, options.Password, StopDB) if err != nil { return instructions, err } @@ -226,7 +232,7 @@ func (vcc *VClusterCommands) produceStopDBInstructions(options *VStopDatabaseOpt // return an error if a requirement isn't met. func (options *VStopDatabaseOptions) checkStopDBRequirements(vdb *VCoordinationDatabase) error { // if stop db on the whole cluster, at least one UP main cluster host in the host list - if options.Sandbox == "" && !options.MainCluster { + if options.SandboxName == "" && !options.MainCluster { hasMainClusterHost := false for _, host := range options.Hosts { vnode, ok := vdb.HostNodeMap[host] diff --git a/vclusterops/stop_node.go b/vclusterops/stop_node.go index 9bd74c7..231cf5b 100644 --- a/vclusterops/stop_node.go +++ b/vclusterops/stop_node.go @@ -31,49 +31,61 @@ type VStopNodeOptions struct { } func VStopNodeOptionsFactory() VStopNodeOptions { - opt := VStopNodeOptions{} + options := VStopNodeOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } -func (o *VStopNodeOptions) setDefaultValues() { - o.DatabaseOptions.setDefaultValues() +func (options *VStopNodeOptions) setDefaultValues() { + options.DatabaseOptions.setDefaultValues() } -func (o *VStopNodeOptions) validateParseOptions(logger vlog.Printer) error { - // validate required parameters - return o.validateBaseOptions("stop_node", logger) +func (options *VStopNodeOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(commandStopNode, logger) + if err != nil { + return err + } + return nil +} + +func (options *VStopNodeOptions) validateParseOptions(logger vlog.Printer) error { + // batch 1: validate required parameters + err := options.validateRequiredOptions(logger) + if err != nil { + return err + } + return nil } // analyzeOptions will modify some options based on what is chosen -func (o *VStopNodeOptions) analyzeOptions() (err error) { - o.StopHosts, err = util.ResolveRawHostsToAddresses(o.StopHosts, o.IPv6) +func (options *VStopNodeOptions) analyzeOptions() (err error) { + options.StopHosts, err = util.ResolveRawHostsToAddresses(options.StopHosts, options.IPv6) if err != nil { return err } // we analyze host names when it is set in user input, otherwise we use hosts in yaml config // resolve RawHosts to be IP addresses - if len(o.RawHosts) > 0 { - o.Hosts, err = util.ResolveRawHostsToAddresses(o.RawHosts, o.IPv6) + if len(options.RawHosts) > 0 { + options.Hosts, err = util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) if err != nil { return err } - o.normalizePaths() + options.normalizePaths() } return nil } -func (o *VStopNodeOptions) validateAnalyzeOptions(logger vlog.Printer) error { - err := o.validateParseOptions(logger) +func (options *VStopNodeOptions) validateAnalyzeOptions(logger vlog.Printer) error { + err := options.validateParseOptions(logger) if err != nil { return err } - return o.analyzeOptions() + return options.analyzeOptions() } // VStopNode stops a host in an existing database. @@ -126,7 +138,7 @@ func checkStopNodeRequirements(vdb *VCoordinationDatabase, hostsToStop []string) // completeVDBSetting sets some VCoordinationDatabase fields we cannot get yet // from the https endpoints. We set those fields from options. -func (o *VStopNodeOptions) completeVDBSetting(vdb *VCoordinationDatabase) { +func (options *VStopNodeOptions) completeVDBSetting(vdb *VCoordinationDatabase) { hostNodeMap := makeVHostNodeMap() for h, vnode := range vdb.HostNodeMap { hostNodeMap[h] = vnode diff --git a/vclusterops/stop_subcluster.go b/vclusterops/stop_subcluster.go index 8241b51..2659e27 100644 --- a/vclusterops/stop_subcluster.go +++ b/vclusterops/stop_subcluster.go @@ -33,11 +33,11 @@ type VStopSubclusterOptions struct { } func VStopSubclusterOptionsFactory() VStopSubclusterOptions { - opt := VStopSubclusterOptions{} + options := VStopSubclusterOptions{} // set default values to the params - opt.setDefaultValues() + options.setDefaultValues() - return opt + return options } func (options *VStopSubclusterOptions) setDefaultValues() { @@ -46,11 +46,19 @@ func (options *VStopSubclusterOptions) setDefaultValues() { } func (options *VStopSubclusterOptions) validateRequiredOptions(log vlog.Printer) error { - err := options.validateBaseOptions(commandStopCluster, log) + err := options.validateBaseOptions(commandStopSubcluster, log) if err != nil { return err } + if options.SCName == "" { + return fmt.Errorf("must specify a subcluster name") + } + + err = util.ValidateScName(options.SCName) + if err != nil { + return err + } return nil } @@ -109,6 +117,7 @@ func (options *VStopSubclusterOptions) validateAnalyzeOptions(log vlog.Printer) return options.analyzeOptions() } +//nolint:dupl func (vcc VClusterCommands) VStopSubcluster(options *VStopSubclusterOptions) error { /* * - Validate Options diff --git a/vclusterops/unsandbox.go b/vclusterops/unsandbox.go index b1bc000..5274334 100644 --- a/vclusterops/unsandbox.go +++ b/vclusterops/unsandbox.go @@ -35,9 +35,9 @@ type VUnsandboxOptions struct { } func VUnsandboxOptionsFactory() VUnsandboxOptions { - opt := VUnsandboxOptions{} - opt.setDefaultValues() - return opt + options := VUnsandboxOptions{} + options.setDefaultValues() + return options } func (options *VUnsandboxOptions) setDefaultValues() { @@ -46,7 +46,7 @@ func (options *VUnsandboxOptions) setDefaultValues() { } func (options *VUnsandboxOptions) validateRequiredOptions(logger vlog.Printer) error { - err := options.validateBaseOptions("unsandbox_subcluster", logger) + err := options.validateBaseOptions(commandUnsandboxSC, logger) if err != nil { return err } @@ -54,6 +54,20 @@ func (options *VUnsandboxOptions) validateRequiredOptions(logger vlog.Printer) e if options.SCName == "" { return fmt.Errorf("must specify a subcluster name") } + + err = util.ValidateScName(options.SCName) + if err != nil { + return err + } + return nil +} + +func (options *VUnsandboxOptions) validateParseOptions(logger vlog.Printer) error { + // batch 1: validate required parameters + err := options.validateRequiredOptions(logger) + if err != nil { + return err + } return nil } @@ -79,8 +93,8 @@ func (options *VUnsandboxOptions) analyzeOptions() (err error) { return nil } -func (options *VUnsandboxOptions) ValidateAnalyzeOptions(vcc VClusterCommands) error { - if err := options.validateRequiredOptions(vcc.Log); err != nil { +func (options *VUnsandboxOptions) ValidateAnalyzeOptions(logger vlog.Printer) error { + if err := options.validateParseOptions(logger); err != nil { return err } return options.analyzeOptions() diff --git a/vclusterops/util/util.go b/vclusterops/util/util.go index ada9ad7..6d3be4d 100644 --- a/vclusterops/util/util.go +++ b/vclusterops/util/util.go @@ -394,6 +394,20 @@ func ValidateUsernameAndPassword(opName string, useHTTPPassword bool, userName s return nil } +func ValidateSQLEndpointData(opName string, useDBPassword bool, userName string, + password *string, dbName string) error { + if userName == "" { + return fmt.Errorf("[%s] should always provide a username for local database connection", opName) + } + if dbName == "" { + return fmt.Errorf("[%s] should always provide a database name for local database connection", opName) + } + if useDBPassword && password == nil { + return fmt.Errorf("[%s] should properly set the password when a password is configured", opName) + } + return nil +} + const ( FileExist = 0 FileNotExist = 1 @@ -472,6 +486,14 @@ func ValidateDBName(dbName string) error { return ValidateName(dbName, "database") } +func ValidateScName(dbName string) error { + return ValidateName(dbName, "subcluster") +} + +func ValidateSandboxName(dbName string) error { + return ValidateName(dbName, "sandbox") +} + // suppress help message for hidden options func SetParserUsage(parser *flag.FlagSet, op string) { fmt.Printf("Usage of %s:\n", op) @@ -580,6 +602,9 @@ func Max[T constraints.Ordered](a, b T) T { // GetPathPrefix returns a path prefix for a (catalog/data/depot) path of a node func GetPathPrefix(path string) string { + if path == "" { + return path + } return filepath.Dir(filepath.Dir(path)) } diff --git a/vclusterops/util/util_test.go b/vclusterops/util/util_test.go index 99041c0..5eb692d 100644 --- a/vclusterops/util/util_test.go +++ b/vclusterops/util/util_test.go @@ -256,7 +256,7 @@ func TestNewErrorFormatVerb(t *testing.T) { assert.EqualError(t, oldErr3, newErr3.Error()) } -func TestValidateDBName(t *testing.T) { +func TestValidateName(t *testing.T) { // positive cases obj := "database" err := ValidateName("test_db", obj) @@ -274,6 +274,9 @@ func TestValidateDBName(t *testing.T) { err = ValidateName("!!??!!db1", obj) assert.ErrorContains(t, err, "invalid character in "+obj+" name: !") + + err = ValidateName("test-db", obj) + assert.ErrorContains(t, err, "invalid character in "+obj+" name: -") } func TestSetEonFlagHelpMsg(t *testing.T) { diff --git a/vclusterops/vcluster_database_options.go b/vclusterops/vcluster_database_options.go index 845920d..060a218 100644 --- a/vclusterops/vcluster_database_options.go +++ b/vclusterops/vcluster_database_options.go @@ -85,22 +85,29 @@ const ( ) const ( - commandCreateDB = "create_db" - commandDropDB = "drop_db" - commandStopDB = "stop_db" - commandStartDB = "start_db" - commandAddNode = "db_add_node" - commandRemoveNode = "db_remove_node" - commandAddCluster = "db_add_subcluster" - commandRemoveCluster = "db_remove_subcluster" - commandStopCluster = "stop_subcluster" - commandSandboxSC = "sandbox_subcluster" - commandUnsandboxSC = "unsandbox_subcluster" - commandShowRestorePoints = "show_restore_points" - commandInstallPackages = "install_packages" - commandConfigRecover = "manage_config_recover" - commandReplicationStart = "replication_start" - commandFetchNodesDetails = "fetch_nodes_details" + commandCreateDB = "create_db" + commandDropDB = "drop_db" + commandStopDB = "stop_db" + commandStartDB = "start_db" + commandAddNode = "add_node" + commandRemoveNode = "remove_node" + commandStopNode = "stop_node" + commandRestartNode = "restart_node" + commandAddSubcluster = "add_subcluster" + commandRemoveSubcluster = "remove_subcluster" + commandStopSubcluster = "stop_subcluster" + commandStartSubcluster = "start_subcluster" + commandSandboxSC = "sandbox_subcluster" + commandUnsandboxSC = "unsandbox_subcluster" + commandShowRestorePoints = "show_restore_points" + commandInstallPackages = "install_packages" + commandConfigRecover = "manage_config_recover" + commandManageConnections = "manage_connections" + commandReplicationStart = "replication_start" + commandFetchNodesDetails = "fetch_nodes_details" + commandAlterSubclusterType = "alter_subcluster_type" + commandRenameSc = "rename_subcluster" + commandReIP = "re_ip" ) func DatabaseOptionsFactory() DatabaseOptions { @@ -141,7 +148,7 @@ func (opt *DatabaseOptions) validateBaseOptions(commandName string, log vlog.Pri // config directory // VER-91801: remove this condition once re_ip supports the config file - if !slices.Contains([]string{"re_ip"}, commandName) { + if !slices.Contains([]string{commandReIP}, commandName) { err = opt.validateConfigDir(commandName) if err != nil { return err @@ -219,7 +226,7 @@ func (opt *DatabaseOptions) validateCatalogPath() error { func (opt *DatabaseOptions) validateConfigDir(commandName string) error { // validate for the following commands only // TODO: add other commands into the command list - commands := []string{commandCreateDB, commandDropDB, commandStopDB, commandStartDB, commandAddCluster, commandRemoveCluster, + commands := []string{commandCreateDB, commandDropDB, commandStopDB, commandStartDB, commandAddSubcluster, commandRemoveSubcluster, commandSandboxSC, commandUnsandboxSC, commandShowRestorePoints, commandAddNode, commandRemoveNode, commandInstallPackages} if slices.Contains(commands, commandName) { return nil @@ -265,6 +272,19 @@ func (opt *DatabaseOptions) setUsePassword(log vlog.Printer) error { return nil } +func (opt *DatabaseOptions) setUsePasswordForLocalDBConnection(log vlog.Printer) error { + opt.usePassword = false + if opt.Password != nil { + opt.usePassword = true + } + // username is always required when local db connection is made + err := opt.validateUserName(log) + if err != nil { + return err + } + return nil +} + // normalizePaths replaces all '//' to be '/', and trim // catalog, data and depot prefixes. func (opt *DatabaseOptions) normalizePaths() { diff --git a/vclusterops/vlog/printer.go b/vclusterops/vlog/printer.go index 1588f26..79ea477 100644 --- a/vclusterops/vlog/printer.go +++ b/vclusterops/vlog/printer.go @@ -20,6 +20,7 @@ import ( "os" "strings" + "github.com/fatih/color" "github.com/go-logr/logr" "github.com/go-logr/zapr" "go.uber.org/zap" @@ -220,3 +221,9 @@ func (p *Printer) SetupOrDie(logFile string) { func isVerboseOutputEnabled() bool { return os.Getenv("VERBOSE_OUTPUT") == "yes" } + +// DisplayColorInfo prints a colored line into console +func DisplayColorInfo(msg string, v ...any) { + clr := color.New(color.FgBlue) + clr.Printf("\u25b6 "+msg+"\n", v...) +}