diff --git a/commands/cluster_command_launcher.go b/commands/cluster_command_launcher.go index c9961b3..07a1c5c 100644 --- a/commands/cluster_command_launcher.go +++ b/commands/cluster_command_launcher.go @@ -69,6 +69,8 @@ const ( configParamKey = "configParam" configParamFileFlag = "config-param-file" configParamFileKey = "configParamFile" + licenseFileFlag = "license-file" + licenseHostFlag = "license-host" logPathFlag = "log-path" logPathKey = "logPath" keyFileFlag = "key-file" @@ -245,6 +247,7 @@ const ( createArchiveCmd = "create_archive" saveRestorePointsSubCmd = "save_restore_point" getDrainingStatusSubCmd = "get_draining_status" + upgradeLicenseCmd = "upgrade_license" ) // cmdGlobals holds global variables shared by multiple @@ -630,6 +633,7 @@ func constructCmds() []*cobra.Command { makeCmdPromoteSandbox(), makeCmdCreateArchive(), makeCmdSaveRestorePoint(), + makeCmdUpgradeLicense(), } } diff --git a/commands/cmd_upgrade_license.go b/commands/cmd_upgrade_license.go new file mode 100644 index 0000000..2844877 --- /dev/null +++ b/commands/cmd_upgrade_license.go @@ -0,0 +1,142 @@ +/* + (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 commands + +import ( + "github.com/spf13/cobra" + "github.com/vertica/vcluster/vclusterops" + "github.com/vertica/vcluster/vclusterops/vlog" +) + +/* CmdUpgradeLicense + * + * Parses arguments to upgrade-license and calls + * the high-level function for upgrade-license. + * + * Implements ClusterCommand interface + */ + +type CmdUpgradeLicense struct { + CmdBase + upgradeLicenseOptions *vclusterops.VUpgradeLicenseOptions +} + +func makeCmdUpgradeLicense() *cobra.Command { + newCmd := &CmdUpgradeLicense{} + opt := vclusterops.VUpgradeLicenseFactory() + newCmd.upgradeLicenseOptions = &opt + + cmd := makeBasicCobraCmd( + newCmd, + upgradeLicenseCmd, + "Upgrade license.", + `Upgrade license. + +Examples: + # Upgrade license + vcluster upgrade_license --license-file LICENSE_FILE --license-host HOST_OF_LICENSE_FILE + + # Upgrade license with connecting using database password + vcluster upgrade_license --license-file LICENSE_FILE --license-host HOST_OF_LICENSE_FILE --password "PASSWORD" +`, + []string{dbNameFlag, configFlag, passwordFlag, + hostsFlag, ipv6Flag}, + ) + + // local flags + newCmd.setLocalFlags(cmd) + + // require license file path + markFlagsRequired(cmd, licenseFileFlag) + markFlagsRequired(cmd, licenseHostFlag) + + return cmd +} + +// setLocalFlags will set the local flags the command has +func (c *CmdUpgradeLicense) setLocalFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + &c.upgradeLicenseOptions.LicenseFilePath, + licenseFileFlag, + "", + "Absolute path of the license file.", + ) + cmd.Flags().StringVar( + &c.upgradeLicenseOptions.LicenseHost, + licenseHostFlag, + "", + "The host the license file located on.", + ) +} + +func (c *CmdUpgradeLicense) Parse(inputArgv []string, logger vlog.Printer) error { + c.argv = inputArgv + logger.LogArgParse(&c.argv) + + // for some options, we do not want to use their default values, + // if they are not provided in cli, + // reset the value of those options to nil + c.ResetUserInputOptions(&c.upgradeLicenseOptions.DatabaseOptions) + + return c.validateParse(logger) +} + +func (c *CmdUpgradeLicense) validateParse(logger vlog.Printer) error { + logger.Info("Called validateParse()") + + err := c.ValidateParseBaseOptions(&c.upgradeLicenseOptions.DatabaseOptions) + if err != nil { + return err + } + + if !c.usePassword() { + err = c.getCertFilesFromCertPaths(&c.upgradeLicenseOptions.DatabaseOptions) + if err != nil { + return err + } + } + err = c.setDBPassword(&c.upgradeLicenseOptions.DatabaseOptions) + if err != nil { + return err + } + + return nil +} + +func (c *CmdUpgradeLicense) Analyze(logger vlog.Printer) error { + logger.Info("Called method Analyze()") + return nil +} + +func (c *CmdUpgradeLicense) Run(vcc vclusterops.ClusterCommands) error { + vcc.LogInfo("Called method Run()") + + options := c.upgradeLicenseOptions + + err := vcc.VUpgradeLicense(options) + if err != nil { + vcc.LogError(err, "failed to upgrade license", "license file", options.LicenseFilePath) + return err + } + + vcc.DisplayInfo("Successfully upgraded license: %s", options.LicenseFilePath) + return nil +} + +// SetDatabaseOptions will assign a vclusterops.DatabaseOptions instance to the one in CmdUpgradeLicense +func (c *CmdUpgradeLicense) SetDatabaseOptions(opt *vclusterops.DatabaseOptions) { + c.upgradeLicenseOptions.DatabaseOptions = *opt +} diff --git a/vclusterops/cluster_op.go b/vclusterops/cluster_op.go index dd8eca9..4e9ec3c 100644 --- a/vclusterops/cluster_op.go +++ b/vclusterops/cluster_op.go @@ -601,6 +601,7 @@ type ClusterCommands interface { VStopNode(options *VStopNodeOptions) error VStopSubcluster(options *VStopSubclusterOptions) error VUnsandbox(options *VUnsandboxOptions) error + VUpgradeLicense(options *VUpgradeLicenseOptions) error } type VClusterCommandsLogger struct { diff --git a/vclusterops/cluster_op_engine_context.go b/vclusterops/cluster_op_engine_context.go index af87bb7..540cf5b 100644 --- a/vclusterops/cluster_op_engine_context.go +++ b/vclusterops/cluster_op_engine_context.go @@ -22,6 +22,7 @@ type opEngineExecContext struct { networkProfiles map[string]networkProfile nmaVDatabase nmaVDatabase upHosts []string // a sorted host list that contains all up nodes + computeHosts []string // a sorted host list that contains all up (COMPUTE) compute nodes nodesInfo []NodeInfo scNodesInfo []NodeInfo // a node list contains all nodes in a subcluster diff --git a/vclusterops/cmd_type.go b/vclusterops/cmd_type.go index 25189ec..3f562ec 100644 --- a/vclusterops/cmd_type.go +++ b/vclusterops/cmd_type.go @@ -43,6 +43,7 @@ const ( RemoveNodeSyncCat CreateArchiveCmd PollSubclusterStateCmd + UpgradeLicenseCmd ) var cmdStringMap = map[CmdType]string{ @@ -84,6 +85,7 @@ var cmdStringMap = map[CmdType]string{ RemoveNodeSyncCat: "remove_node_sync_cat", CreateArchiveCmd: "create_archive", PollSubclusterStateCmd: "poll_subcluster_state", + UpgradeLicenseCmd: "upgrade_license", } func (cmd CmdType) CmdString() string { diff --git a/vclusterops/coordinator_database.go b/vclusterops/coordinator_database.go index 7eed111..c4b1323 100644 --- a/vclusterops/coordinator_database.go +++ b/vclusterops/coordinator_database.go @@ -162,8 +162,16 @@ func (vdb *VCoordinationDatabase) addNode(vnode *VCoordinationNode) error { // in all clusters (main and sandboxes) func (vdb *VCoordinationDatabase) addHosts(hosts []string, scName string, existingHostNodeMap vHostNodeMap) error { - totalHostCount := len(hosts) + len(existingHostNodeMap) + totalHostCount := len(hosts) + len(existingHostNodeMap) + len(vdb.UnboundNodes) nodeNameToHost := genNodeNameToHostMap(existingHostNodeMap) + // The GenVNodeName(...) function below will generate node names based on nodeNameToHost and totalHostCount. + // If a name already exists, it won't be re-generated. + // In this case, we need to add unbound node names into this map too. + // Otherwise, the new nodes will reuse the existing unbound node names, then make a clash later on. + for _, vnode := range vdb.UnboundNodes { + nodeNameToHost[vnode.Name] = vnode.Address + } + for _, host := range hosts { vNode := makeVCoordinationNode() name, ok := util.GenVNodeName(nodeNameToHost, vdb.Name, totalHostCount) @@ -339,13 +347,13 @@ func (vdb *VCoordinationDatabase) filterUpHostlist(inputHosts []string, sandbox // host address not found in vdb, skip it continue } - if vnode.Sandbox == "" && vnode.State == util.NodeUpState { + if vnode.Sandbox == util.MainClusterSandbox && vnode.State == util.NodeUpState { clusterHosts = append(clusterHosts, vnode.Address) } else if vnode.Sandbox == sandbox && vnode.State == util.NodeUpState { upSandboxHosts = append(upSandboxHosts, vnode.Address) } } - if sandbox == "" { + if sandbox == util.MainClusterSandbox { return clusterHosts } return upSandboxHosts diff --git a/vclusterops/fetch_database.go b/vclusterops/fetch_database.go index 40db0c3..b863e98 100644 --- a/vclusterops/fetch_database.go +++ b/vclusterops/fetch_database.go @@ -122,6 +122,9 @@ func (vcc VClusterCommands) VFetchCoordinationDatabase(options *VFetchCoordinati } for h, n := range nmaVDB.HostNodeMap { + if h == util.UnboundedIPv4 || h == util.UnboundedIPv6 { + continue + } vnode, ok := vdb.HostNodeMap[h] if !ok { return vdb, fmt.Errorf("host %s is not found in the vdb object", h) diff --git a/vclusterops/https_check_subcluster_sandbox_op.go b/vclusterops/https_check_subcluster_sandbox_op.go index 530a87a..63c38a8 100644 --- a/vclusterops/https_check_subcluster_sandbox_op.go +++ b/vclusterops/https_check_subcluster_sandbox_op.go @@ -18,6 +18,8 @@ package vclusterops import ( "errors" "fmt" + + "github.com/vertica/vcluster/vclusterops/util" ) type httpsCheckSubclusterSandboxOp struct { @@ -60,6 +62,10 @@ func (op *httpsCheckSubclusterSandboxOp) setupClusterHTTPRequest(hosts []string) } func (op *httpsCheckSubclusterSandboxOp) prepare(execContext *opEngineExecContext) error { + if execContext.computeHosts != nil { + op.hosts = util.SliceDiff(op.hosts, execContext.computeHosts) + } + execContext.dispatcher.setup(op.hosts) return op.setupClusterHTTPRequest(op.hosts) diff --git a/vclusterops/https_create_archive_op.go b/vclusterops/https_create_archive_op.go index 343e281..ae699c0 100644 --- a/vclusterops/https_create_archive_op.go +++ b/vclusterops/https_create_archive_op.go @@ -114,7 +114,7 @@ func (op *httpsCreateArchiveOp) processResult(_ *opEngineExecContext) error { var allErrs error // every host needs to have a successful result, otherwise we fail this op - // because we want depot created successfully on all hosts + // because we want archives to be created for host, result := range op.clusterHTTPRequest.ResultCollection { op.logResponse(host, result) diff --git a/vclusterops/https_get_up_nodes_op.go b/vclusterops/https_get_up_nodes_op.go index cdaec9d..12f2821 100644 --- a/vclusterops/https_get_up_nodes_op.go +++ b/vclusterops/https_get_up_nodes_op.go @@ -138,6 +138,7 @@ func (op *httpsGetUpNodesOp) execute(execContext *opEngineExecContext) error { func (op *httpsGetUpNodesOp) processResult(execContext *opEngineExecContext) error { var allErrs error upHosts := mapset.NewSet[string]() + computeHosts := mapset.NewSet[string]() upScInfo := make(map[string]string) exceptionHosts := []string{} downHosts := []string{} @@ -148,8 +149,9 @@ func (op *httpsGetUpNodesOp) processResult(execContext *opEngineExecContext) err op.logResponse(host, result) if !result.isPassing() { allErrs = errors.Join(allErrs, result.err) - if result.isUnauthorizedRequest() || result.isInternalError() { - // Authentication error and any unexpected internal server error + if result.isUnauthorizedRequest() || result.isInternalError() || result.hasPreconditionFailed() { + // Authentication error and any unexpected internal server error, plus compute nodes or nodes + // that haven't joined the cluster yet exceptionHosts = append(exceptionHosts, host) continue } @@ -167,16 +169,15 @@ func (op *httpsGetUpNodesOp) processResult(execContext *opEngineExecContext) err continue } - if op.cmdType == StopDBCmd || op.cmdType == StopSubclusterCmd { - err = op.validateHosts(nodesStates) - if err != nil { - allErrs = errors.Join(allErrs, err) - break - } + // For certain commands, check hosts in input against those reported from endpoint + err = op.validateHosts(nodesStates) + if err != nil { + allErrs = errors.Join(allErrs, err) + break } // Collect all the up hosts - err = op.collectUpHosts(nodesStates, host, upHosts, upScInfo, sandboxInfo, upScNodes, scNodes) + err = op.collectUpHosts(nodesStates, host, upHosts, computeHosts, upScInfo, sandboxInfo, upScNodes, scNodes) if err != nil { allErrs = errors.Join(allErrs, err) return allErrs @@ -190,6 +191,7 @@ func (op *httpsGetUpNodesOp) processResult(execContext *opEngineExecContext) err break } } + execContext.computeHosts = computeHosts.ToSlice() execContext.nodesInfo = upScNodes.ToSlice() execContext.scNodesInfo = scNodes.ToSlice() execContext.upHostsToSandboxes = sandboxInfo @@ -275,6 +277,10 @@ func (op *httpsGetUpNodesOp) processHostLists(upHosts mapset.Set[string], upScIn // validateHosts can validate if hosts in user input matches the ones in GET /nodes response func (op *httpsGetUpNodesOp) validateHosts(nodesStates nodesStateInfo) error { + // only needed for the following commands + if !(op.cmdType == StopDBCmd || op.cmdType == StopSubclusterCmd) { + return nil + } var dbHosts []string dbUnexpected := false unexpectedDBName := "" @@ -310,7 +316,7 @@ func (op *httpsGetUpNodesOp) checkUpHostEligible(node *nodeStateInfo) bool { return true } -func (op *httpsGetUpNodesOp) collectUpHosts(nodesStates nodesStateInfo, host string, upHosts mapset.Set[string], +func (op *httpsGetUpNodesOp) collectUpHosts(nodesStates nodesStateInfo, host string, upHosts, computeHosts mapset.Set[string], upScInfo, sandboxInfo map[string]string, upScNodes, scNodes mapset.Set[NodeInfo]) (err error) { foundSC := false for _, node := range nodesStates.NodeList { @@ -333,6 +339,10 @@ func (op *httpsGetUpNodesOp) collectUpHosts(nodesStates nodesStateInfo, host str } } + if node.State == util.NodeComputeState { + computeHosts.Add(node.Address) + } + if op.scName == node.Subcluster { op.sandbox = node.Sandbox if node.IsPrimary { diff --git a/vclusterops/https_install_license_op.go b/vclusterops/https_install_license_op.go new file mode 100644 index 0000000..83a603a --- /dev/null +++ b/vclusterops/https_install_license_op.go @@ -0,0 +1,124 @@ +/* + (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 httpsInstallLicenseOp struct { + opBase + opHTTPSBase + LicenseFilePath string +} + +// makeHTTPSInstallLicenseOp will make an op that call vertica-https service to install license for database +// this op is a global op, so it should only be sent to one host of the DB group +func makeHTTPSInstallLicenseOp(hosts []string, useHTTPPassword bool, userName string, + httpsPassword *string, licenseFilePath string) (httpsInstallLicenseOp, error) { + op := httpsInstallLicenseOp{} + op.name = "HTTPSInstallLicenseOp" + op.description = "Install license for database" + op.hosts = hosts + op.useHTTPPassword = useHTTPPassword + if useHTTPPassword { + err := util.ValidateUsernameAndPassword(op.name, useHTTPPassword, userName) + if err != nil { + return op, err + } + op.userName = userName + op.httpsPassword = httpsPassword + } + op.LicenseFilePath = licenseFilePath + return op, nil +} + +func (op *httpsInstallLicenseOp) setupClusterHTTPRequest(hosts []string) error { + for _, host := range hosts { + httpRequest := hostHTTPRequest{} + httpRequest.Method = PutMethod + httpRequest.buildHTTPSEndpoint(util.LicenseEndpoint) + httpRequest.QueryParams = map[string]string{"licenseFile": op.LicenseFilePath} + if op.useHTTPPassword { + httpRequest.Password = op.httpsPassword + httpRequest.Username = op.userName + } + op.clusterHTTPRequest.RequestCollection[host] = httpRequest + } + + return nil +} + +func (op *httpsInstallLicenseOp) prepare(execContext *opEngineExecContext) error { + execContext.dispatcher.setup(op.hosts) + + return op.setupClusterHTTPRequest(op.hosts) +} + +func (op *httpsInstallLicenseOp) execute(execContext *opEngineExecContext) error { + if err := op.runExecute(execContext); err != nil { + return err + } + + return op.processResult(execContext) +} + +func (op *httpsInstallLicenseOp) processResult(_ *opEngineExecContext) error { + var allErrs error + + // should only send request to one host as upgrade license is a global op + // using for-loop here for accommodating potential future cases for sandboxes + for host, result := range op.clusterHTTPRequest.ResultCollection { + op.logResponse(host, result) + + if result.isUnauthorizedRequest() { + return fmt.Errorf("[%s] wrong password/certificate for https service on host %s", op.name, host) + } + + if !result.isPassing() { + allErrs = errors.Join(allErrs, result.err) + continue + } + + // upgrade license succeeds + // the successful response object looks like the following: + /* { + "detail":"Success: Replacing vertica license: + CompanyName: Vertica Systems, Inc. + start_date: YYYY-MM-DD + end_date: YYYY-MM-DD + grace_period: 0 + capacity: Unlimited + Node Limit: Unlimited + " + } + */ + _, 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) + } + // upgrade succeeds, return now + return nil + } + return allErrs +} + +func (op *httpsInstallLicenseOp) finalize(_ *opEngineExecContext) error { + return nil +} diff --git a/vclusterops/nma_download_file_op.go b/vclusterops/nma_download_file_op.go index 23c2aa1..284ee68 100644 --- a/vclusterops/nma_download_file_op.go +++ b/vclusterops/nma_download_file_op.go @@ -208,6 +208,7 @@ type fileContent struct { Path string `json:"path"` Usage int `json:"usage"` } `json:"StorageLocation"` + Sandbox string } func (op *nmaDownloadFileOp) processResult(execContext *opEngineExecContext) error { @@ -278,7 +279,7 @@ func (op *nmaDownloadFileOp) processResult(execContext *opEngineExecContext) err } // save descFileContent in vdb - return op.buildVDBFromClusterConfig(descFileContent) + return op.buildVDBFromClusterConfig(&descFileContent) } httpsErr := errors.Join(fmt.Errorf("[%s] HTTPS call failed on host %s", op.name, host), result.err) @@ -299,13 +300,14 @@ func filterPrimaryNodes(descFileContent *fileContent) { } // buildVDBFromClusterConfig can build a vdb using cluster_config.json -func (op *nmaDownloadFileOp) buildVDBFromClusterConfig(descFileContent fileContent) error { +func (op *nmaDownloadFileOp) buildVDBFromClusterConfig(descFileContent *fileContent) error { op.vdb.HostNodeMap = makeVHostNodeMap() for _, node := range descFileContent.NodeList { vNode := makeVCoordinationNode() vNode.Name = node.Name vNode.Address = node.Address vNode.IsPrimary = node.IsPrimary + vNode.Sandbox = descFileContent.Sandbox // remove suffix "/Catalog" from node catalog path // e.g. /data/test_db/v_test_db_node0002_catalog/Catalog -> /data/test_db/v_test_db_node0002_catalog diff --git a/vclusterops/start_db.go b/vclusterops/start_db.go index 9768e16..622a50c 100644 --- a/vclusterops/start_db.go +++ b/vclusterops/start_db.go @@ -133,14 +133,12 @@ func (vcc VClusterCommands) VStartDatabase(options *VStartDatabaseOptions) (vdbP // VER-93369 may improve this if the CLI knows which nodes are primary // from the config file var vdb VCoordinationDatabase - // retrieve database information from cluster_config.json for Eon databases, - // skip this step for starting a sandbox because cluster_config.json does not - // contain accurate info of nodes in a sandbox - if !options.HostsInSandbox && options.IsEon { + // retrieve database information from cluster_config.json for Eon databases + if options.IsEon { const warningMsg = " for an Eon database, start_db after revive_db could fail " + util.DBInfo if options.CommunalStorageLocation != "" { - vdbNew, e := options.getVDBWhenDBIsDown(vcc) + vdbNew, e := options.getVDBFromSandboxWhenDBIsDown(vcc, options.Sandbox) if e != nil { // show a warning message if we cannot get VDB from a down database vcc.Log.PrintWarning(util.CommStorageFail + warningMsg) @@ -173,7 +171,7 @@ func (vcc VClusterCommands) VStartDatabase(options *VStartDatabaseOptions) (vdbP clusterOpEngine := makeClusterOpEngine(instructions, options) // Give the instructions to the VClusterOpEngine to run - runError := clusterOpEngine.run(vcc.Log) + runError := clusterOpEngine.runInSandbox(vcc.Log, &vdb, options.Sandbox) if runError != nil { return nil, fmt.Errorf("fail to start database: %w", runError) } diff --git a/vclusterops/upgrade_license.go b/vclusterops/upgrade_license.go new file mode 100644 index 0000000..8bd245d --- /dev/null +++ b/vclusterops/upgrade_license.go @@ -0,0 +1,175 @@ +/* + (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 VUpgradeLicenseOptions struct { + DatabaseOptions + + // Required arguments + LicenseFilePath string + LicenseHost string +} + +func VUpgradeLicenseFactory() VUpgradeLicenseOptions { + options := VUpgradeLicenseOptions{} + // set default values to the params + options.setDefaultValues() + + return options +} + +func (options *VUpgradeLicenseOptions) setDefaultValues() { + options.DatabaseOptions.setDefaultValues() +} + +func (options *VUpgradeLicenseOptions) validateRequiredOptions(logger vlog.Printer) error { + err := options.validateBaseOptions(UpgradeLicenseCmd, logger) + if err != nil { + return err + } + if options.LicenseFilePath == "" { + return fmt.Errorf("must specify a license file") + } + if options.LicenseHost == "" { + return fmt.Errorf("must specify a host the license file located on") + } + // license file must be specified as an absolute path + err = util.ValidateAbsPath(options.LicenseFilePath, "license file path") + if err != nil { + return err + } + return nil +} + +func (options *VUpgradeLicenseOptions) validateParseOptions(log vlog.Printer) error { + // validate required parameters + err := options.validateRequiredOptions(log) + if err != nil { + return err + } + + err = options.validateAuthOptions(UpgradeLicenseCmd.CmdString(), log) + if err != nil { + return err + } + + return nil +} + +// analyzeOptions will modify some options based on what is chosen +func (options *VUpgradeLicenseOptions) analyzeOptions() (err error) { + // resolve license host to be IP addresses + licenseHostAddr, err := util.ResolveToOneIP(options.LicenseHost, options.IPv6) + if err != nil { + return err + } + // install license call has to be done on the host that has the license file + options.LicenseHost = licenseHostAddr + if len(options.RawHosts) > 0 { + // resolve RawHosts to be IP addresses + hostAddresses, err := util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) + if err != nil { + return err + } + options.Hosts = hostAddresses + } + return nil +} + +func (options *VUpgradeLicenseOptions) validateAnalyzeOptions(log vlog.Printer) error { + if err := options.validateParseOptions(log); err != nil { + return err + } + if err := options.analyzeOptions(); err != nil { + return err + } + if err := options.setUsePassword(log); err != nil { + return err + } + return options.validateUserName(log) +} + +func (vcc VClusterCommands) VUpgradeLicense(options *VUpgradeLicenseOptions) 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 + } + + // produce create acchive instructions + instructions, err := vcc.produceUpgradeLicenseInstructions(options) + if err != nil { + return fmt.Errorf("fail to produce instructions, %w", err) + } + + // create a VClusterOpEngine, and add certs to the engine + clusterOpEngine := makeClusterOpEngine(instructions, options) + + // give the instructions to the VClusterOpEngine to run + runError := clusterOpEngine.run(vcc.Log) + if runError != nil { + return fmt.Errorf("fail to upgrade license: %w", runError) + } + return nil +} + +// The generated instructions will later perform the following operations necessary +// for a successful create_archive: +// - Run install license API +func (vcc *VClusterCommands) produceUpgradeLicenseInstructions(options *VUpgradeLicenseOptions) ([]clusterOp, error) { + var instructions []clusterOp + vdb := makeVCoordinationDatabase() + + err := vcc.getVDBFromRunningDB(&vdb, &options.DatabaseOptions) + if err != nil { + return instructions, err + } + + // get up hosts + hosts := options.Hosts + // Trim host list + hosts = vdb.filterUpHostlist(hosts, util.MainClusterSandbox) + // if license host isn't an UP host, error out + // this license upgrade has to be done in main cluster + if !util.StringInArray(options.LicenseHost, hosts) { + return instructions, fmt.Errorf("license file must be on an UP host, the specified host %s is not UP", options.LicenseHost) + } + + initiatorHost := []string{options.LicenseHost} + + httpsInstallLicenseOp, err := makeHTTPSInstallLicenseOp(initiatorHost, options.usePassword, + options.UserName, options.Password, options.LicenseFilePath) + if err != nil { + return instructions, err + } + + instructions = append(instructions, + &httpsInstallLicenseOp) + return instructions, nil +} diff --git a/vclusterops/util/util.go b/vclusterops/util/util.go index 12d2f32..ae0b7df 100644 --- a/vclusterops/util/util.go +++ b/vclusterops/util/util.go @@ -67,6 +67,7 @@ const ( NodesEndpoint = "nodes/" DropEndpoint = "/drop" ArchiveEndpoint = "archives" + LicenseEndpoint = "license" ) const ( diff --git a/vclusterops/vcluster_database_options.go b/vclusterops/vcluster_database_options.go index 1cb38e6..db74f0a 100644 --- a/vclusterops/vcluster_database_options.go +++ b/vclusterops/vcluster_database_options.go @@ -288,8 +288,15 @@ func (opt *DatabaseOptions) normalizePaths() { opt.DepotPrefix = util.GetCleanPath(opt.DepotPrefix) } -// getVDBWhenDBIsDown can retrieve db configurations from NMA /nodes endpoint and cluster_config.json when db is down +// getVDBWhenDBIsDown can retrieve db configurations from the NMA /nodes endpoint and cluster_config.json when db is down func (opt *DatabaseOptions) getVDBWhenDBIsDown(vcc VClusterCommands) (vdb VCoordinationDatabase, err error) { + return opt.getVDBFromSandboxWhenDBIsDown(vcc, util.MainClusterSandbox) +} + +// getVDBFromSandboxWhenDBIsDown can retrieve db configurations about a given sandbox +// from the NMA /nodes endpoint and cluster_config.json when db is down +func (opt *DatabaseOptions) getVDBFromSandboxWhenDBIsDown(vcc VClusterCommands, + sandbox string) (vdb VCoordinationDatabase, err error) { /* * 1. Get node names for input hosts from NMA /nodes. * 2. Get other node information for input hosts from cluster_config.json. @@ -324,7 +331,7 @@ func (opt *DatabaseOptions) getVDBWhenDBIsDown(vcc VClusterCommands) (vdb VCoord // step 2: get node details from cluster_config.json vdb2 := VCoordinationDatabase{} var instructions2 []clusterOp - currConfigFileSrcPath := opt.getCurrConfigFilePath(util.MainClusterSandbox) + currConfigFileSrcPath := opt.getCurrConfigFilePath(sandbox) nmaDownLoadFileOp, err := makeNMADownloadFileOp(opt.Hosts, currConfigFileSrcPath, currConfigFileDestPath, catalogPath, opt.ConfigurationParameters, &vdb2) if err != nil {