From 0b1e7d932d33773eaa8bb9160693adbf10b23f26 Mon Sep 17 00:00:00 2001 From: releng Date: Wed, 11 Dec 2024 16:21:00 -0500 Subject: [PATCH] Sync from server repo (9416716a171) --- commands/cluster_command_launcher.go | 5 + commands/cmd_create_db.go | 6 + commands/cmd_re_ip.go | 10 +- commands/cmd_upgrade_license.go | 142 +++++++++++ commands/user_input_test.go | 16 ++ vclusterops/cluster_op.go | 12 + vclusterops/cluster_op_engine_context.go | 1 + vclusterops/cmd_type.go | 2 + vclusterops/coordinator_database.go | 73 +++++- vclusterops/create_db.go | 24 ++ vclusterops/fetch_database.go | 3 + vclusterops/fetch_node_state.go | 2 +- vclusterops/helpers.go | 30 +++ .../https_check_subcluster_sandbox_op.go | 6 + vclusterops/https_create_archive_op.go | 2 +- .../https_create_tls_authentication_op.go | 114 +++++++++ vclusterops/https_get_up_nodes_op.go | 41 +++- vclusterops/https_grant_authentication_op.go | 113 +++++++++ vclusterops/https_install_license_op.go | 124 ++++++++++ vclusterops/nma_download_file_op.go | 6 +- vclusterops/nma_read_catalog_editor_op.go | 8 + vclusterops/re_ip.go | 36 ++- vclusterops/replication.go | 4 +- vclusterops/start_db.go | 10 +- vclusterops/start_node.go | 26 +- vclusterops/stop_db.go | 22 +- vclusterops/stop_node.go | 2 +- vclusterops/unsandbox.go | 228 +++++++++++++++--- vclusterops/upgrade_license.go | 175 ++++++++++++++ vclusterops/util/defaults.go | 10 + vclusterops/util/util.go | 19 ++ vclusterops/vcluster_database_options.go | 14 +- 32 files changed, 1196 insertions(+), 90 deletions(-) create mode 100644 commands/cmd_upgrade_license.go create mode 100644 vclusterops/https_create_tls_authentication_op.go create mode 100644 vclusterops/https_grant_authentication_op.go create mode 100644 vclusterops/https_install_license_op.go create mode 100644 vclusterops/upgrade_license.go diff --git a/commands/cluster_command_launcher.go b/commands/cluster_command_launcher.go index c9961b3..b46c3d2 100644 --- a/commands/cluster_command_launcher.go +++ b/commands/cluster_command_launcher.go @@ -63,12 +63,15 @@ const ( archiveNameKey = "archiveName" ipv6Flag = "ipv6" ipv6Key = "ipv6" + enableTLSAuthFlag = "enable-tls-authentication" eonModeFlag = "eon-mode" eonModeKey = "eonMode" configParamFlag = "config-param" configParamKey = "configParam" configParamFileFlag = "config-param-file" configParamFileKey = "configParamFile" + licenseFileFlag = "license-file" + licenseHostFlag = "license-host" logPathFlag = "log-path" logPathKey = "logPath" keyFileFlag = "key-file" @@ -245,6 +248,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 +634,7 @@ func constructCmds() []*cobra.Command { makeCmdPromoteSandbox(), makeCmdCreateArchive(), makeCmdSaveRestorePoint(), + makeCmdUpgradeLicense(), } } diff --git a/commands/cmd_create_db.go b/commands/cmd_create_db.go index a505ff3..722bb6f 100644 --- a/commands/cmd_create_db.go +++ b/commands/cmd_create_db.go @@ -171,6 +171,12 @@ func (c *CmdCreateDB) setLocalFlags(cmd *cobra.Command) { util.GetEnvInt("NODE_STATE_POLLING_TIMEOUT", util.DefaultTimeoutSeconds), "The time, in seconds, to wait for the nodes to start after database creation (default: 300).", ) + cmd.Flags().BoolVar( + &c.createDBOptions.EnableTLSAuth, + enableTLSAuthFlag, + false, + "Enable TLS authentication for all users after database creation", + ) c.setSpreadlFlags(cmd) } diff --git a/commands/cmd_re_ip.go b/commands/cmd_re_ip.go index d90b932..d2133b8 100644 --- a/commands/cmd_re_ip.go +++ b/commands/cmd_re_ip.go @@ -59,7 +59,7 @@ This file should only include the IP addresses of nodes that you want to update. Examples: # Alter the IP address of database nodes with user input vcluster re_ip --db-name test_db --hosts 10.20.30.40,10.20.30.41,10.20.30.42 \ - --catalog-path /data --re-ip-file /data/re_ip_map.json \ + --catalog-path /data --re-ip-file /data/re_ip_map.json --sandbox sand \ --password "PASSWORD" # Alter the IP address of database nodes with config file @@ -67,7 +67,7 @@ Examples: --config /opt/vertica/config/vertica_cluster.yaml \ --password "PASSWORD" `, - []string{dbNameFlag, hostsFlag, ipv6Flag, catalogPathFlag, configParamFlag, configFlag}, + []string{dbNameFlag, hostsFlag, ipv6Flag, catalogPathFlag, configParamFlag, configFlag, sandboxFlag}, ) // local flags @@ -88,6 +88,12 @@ func (c *CmdReIP) setLocalFlags(cmd *cobra.Command) { "", "Path of the re-ip file", ) + cmd.Flags().StringVar( + &c.reIPOptions.SandboxName, + sandboxFlag, + "", + "The name of the sandbox. Required if the re-ip hosts are in a sandbox.", + ) } func (c *CmdReIP) Parse(inputArgv []string, logger vlog.Printer) error { 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/commands/user_input_test.go b/commands/user_input_test.go index 6fef8c2..6761762 100644 --- a/commands/user_input_test.go +++ b/commands/user_input_test.go @@ -24,6 +24,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/vertica/vcluster/vclusterops" "gopkg.in/yaml.v3" ) @@ -90,6 +91,21 @@ func TestManageReplication(t *testing.T) { assert.ErrorContains(t, err, `unknown command "test" for "vcluster replication start"`) } +func TestAsyncReplicationErrorMessage(t *testing.T) { + vcommand := vclusterops.VClusterCommands{} + replicationDatabaseOptions := vclusterops.VReplicationDatabaseFactory() + replicationDatabaseOptions.DBName = "db" + replicationDatabaseOptions.Hosts = []string{"12.34.56.78"} + replicationDatabaseOptions.IsEon = true + password := "password" + replicationDatabaseOptions.Password = &password + replicationDatabaseOptions.TargetDB.Hosts = []string{"23.45.67.89"} + replicationDatabaseOptions.TargetDB.DBName = "targetDb" + replicationDatabaseOptions.TableOrSchemaName = ".ns1.s1.*" + _, err := vcommand.VReplicateDatabase(&replicationDatabaseOptions) + assert.ErrorContains(t, err, "not allowed in --table-or-schema-name. HINT:") +} + func TestCreateConnectionFileWrongFileType(t *testing.T) { // vertica_connection.txt will not be created and a unique name is not required var tempConnFilePath = filepath.Join(os.TempDir(), "vertica_connection.txt") diff --git a/vclusterops/cluster_op.go b/vclusterops/cluster_op.go index dd8eca9..4023da7 100644 --- a/vclusterops/cluster_op.go +++ b/vclusterops/cluster_op.go @@ -168,6 +168,17 @@ func (hostResult *hostHTTPResult) isEOF() bool { return hostResult.status == EOFEXCEPTION } +// process a single result, return the error in the result +func (hostResult *hostHTTPResult) getError(host, opName string) error { + if hostResult.isUnauthorizedRequest() { + return fmt.Errorf("[%s] wrong password/certificates for https service on host %s", opName, host) + } + if !hostResult.isPassing() { + return hostResult.err + } + return nil +} + // getStatusString converts ResultStatus to string func (status resultStatus) getStatusString() string { if status == FAILURE { @@ -601,6 +612,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..67cfff2 100644 --- a/vclusterops/coordinator_database.go +++ b/vclusterops/coordinator_database.go @@ -59,6 +59,8 @@ type VCoordinationDatabase struct { PrimaryUpNodes []string ComputeNodes []string FirstStartAfterRevive bool + + AllSandboxes []string // slice of all sandboxes in the cluster } type vHostNodeMap map[string]*VCoordinationNode @@ -108,6 +110,39 @@ func (vdb *VCoordinationDatabase) setFromBasicDBOptions(options *VCreateDatabase return nil } +// update vdb object with sandbox info of given sandbox name +func (vdb *VCoordinationDatabase) updateSandboxNodeInfo(sandVdb *VCoordinationDatabase, sandboxName string) { + for _, vnode := range sandVdb.HostNodeMap { + if vnode.Sandbox == sandboxName { + vdb.HostNodeMap[vnode.Address] = vnode + vdb.HostList = append(vdb.HostList, vnode.Address) + } + } +} + +// populate vdb with main cluster info +func (vdb *VCoordinationDatabase) setMainCluster(mainVdb *VCoordinationDatabase) { + allSandboxes := mapset.NewSet[string]() + vdb.IsEon = mainVdb.IsEon + vdb.UseDepot = mainVdb.UseDepot + vdb.Name = mainVdb.Name + vdb.CommunalStorageLocation = mainVdb.CommunalStorageLocation + vdb.UnboundNodes = mainVdb.UnboundNodes + vdb.PrimaryUpNodes = mainVdb.PrimaryUpNodes + vdb.ComputeNodes = mainVdb.ComputeNodes + vdb.CatalogPrefix = mainVdb.CatalogPrefix + vdb.HostNodeMap = makeVHostNodeMap() + for _, vnode := range mainVdb.HostNodeMap { + if vnode.Sandbox == util.MainClusterSandbox { + vdb.HostNodeMap[vnode.Address] = vnode + vdb.HostList = append(vdb.HostList, vnode.Address) + } else if !allSandboxes.Contains(vnode.Sandbox) { + allSandboxes.Add(vnode.Sandbox) + } + } + vdb.AllSandboxes = allSandboxes.ToSlice() +} + func (vdb *VCoordinationDatabase) setFromCreateDBOptions(options *VCreateDatabaseOptions, logger vlog.Printer) error { // build after validating the options err := options.validateAnalyzeOptions(logger) @@ -162,8 +197,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) @@ -201,6 +244,7 @@ func (vdb *VCoordinationDatabase) copy(targetHosts []string) VCoordinationDataba Ipv6: vdb.Ipv6, PrimaryUpNodes: util.CopySlice(vdb.PrimaryUpNodes), ComputeNodes: util.CopySlice(vdb.ComputeNodes), + AllSandboxes: util.CopySlice(vdb.AllSandboxes), } if len(targetHosts) == 0 { @@ -339,13 +383,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 @@ -380,6 +424,29 @@ type VCoordinationNode struct { ControlNode string } +func CloneVCoordinationNode(node *VCoordinationNode) *VCoordinationNode { + if node == nil { + return nil + } + return &VCoordinationNode{ + Name: node.Name, + Address: node.Address, + CatalogPath: node.CatalogPath, + StorageLocations: append([]string{}, node.StorageLocations...), // Create a new slice + UserStorageLocations: append([]string{}, node.UserStorageLocations...), // Create a new slice + DepotPath: node.DepotPath, + Port: node.Port, + ControlAddressFamily: node.ControlAddressFamily, + IsPrimary: node.IsPrimary, + State: node.State, + Subcluster: node.Subcluster, + Sandbox: node.Sandbox, + Version: node.Version, + IsControlNode: node.IsControlNode, + ControlNode: node.ControlNode, + } +} + func makeVCoordinationNode() VCoordinationNode { return VCoordinationNode{} } diff --git a/vclusterops/create_db.go b/vclusterops/create_db.go index 3ecc336..ed89791 100644 --- a/vclusterops/create_db.go +++ b/vclusterops/create_db.go @@ -34,6 +34,7 @@ type VCreateDatabaseOptions struct { Policy string // database restart policy SQLFile string // SQL file to run (as dbadmin) immediately on database creation LicensePathOnNode string // required to be a fully qualified path + EnableTLSAuth bool // enable TLS authentication immediately on database creation /* part 2: eon db info */ @@ -94,6 +95,8 @@ func (options *VCreateDatabaseOptions) setDefaultValues() { options.LargeCluster = util.DefaultLargeCluster options.ClientPort = util.DefaultClientPort options.SpreadLoggingLevel = util.DefaultSpreadLoggingLevel + // specify whether to enable TLS authentication method upon database creation + options.EnableTLSAuth = false } func (options *VCreateDatabaseOptions) validateRequiredOptions(logger vlog.Printer) error { @@ -339,6 +342,7 @@ func (vcc VClusterCommands) VCreateDatabase(options *VCreateDatabaseOptions) (VC // - Create depot (Eon mode only) // - Mark design ksafe // - Install packages +// - Enable TLS authentication if needed // - Sync catalog func (vcc VClusterCommands) produceCreateDBInstructions( vdb *VCoordinationDatabase, @@ -543,7 +547,27 @@ func (vcc VClusterCommands) produceAdditionalCreateDBInstructions(vdb *VCoordina } instructions = append(instructions, &httpsInstallPackagesOp) } + if options.EnableTLSAuth { + authName := util.DefaultIPv4AuthName + authHosts := util.DefaultIPv4AuthHosts + if options.IPv6 { + authName = util.DefaultIPv6AuthName + authHosts = util.DefaultIPv6AuthHosts + } + httpsCreateTLSAuthOp, err := makeHTTPSCreateTLSAuthOp(bootstrapHost, true /* use password */, username, options.Password, + authName, authHosts) + if err != nil { + return instructions, err + } + instructions = append(instructions, &httpsCreateTLSAuthOp) + httpsGrantTLSAuthOp, err := makeHTTPSGrantTLSAuthOp(bootstrapHost, true /* use password */, username, options.Password, + authName, username /*grantee of tls auth*/) + if err != nil { + return instructions, err + } + instructions = append(instructions, &httpsGrantTLSAuthOp) + } if vdb.IsEon { httpsSyncCatalogOp, err := makeHTTPSSyncCatalogOp(bootstrapHost, true, username, options.Password, CreateDBSyncCat) if err != nil { 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/fetch_node_state.go b/vclusterops/fetch_node_state.go index dddc6d9..19b422a 100644 --- a/vclusterops/fetch_node_state.go +++ b/vclusterops/fetch_node_state.go @@ -194,7 +194,7 @@ func buildNodeStateList(vdb *VCoordinationDatabase, forDownDatabase bool) []Node nodeInfo.IsPrimary = n.IsPrimary nodeInfo.Name = n.Name nodeInfo.Sandbox = n.Sandbox - if forDownDatabase { + if forDownDatabase && n.State == "" { nodeInfo.State = util.NodeDownState } else { nodeInfo.State = n.State diff --git a/vclusterops/helpers.go b/vclusterops/helpers.go index b95f0dd..7e3fc08 100644 --- a/vclusterops/helpers.go +++ b/vclusterops/helpers.go @@ -227,6 +227,36 @@ func (vcc VClusterCommands) getVDBFromMainRunningDBContainsSandbox(vdb *VCoordin true /*update node state by sending http request to each node*/) } +// getDeepVDBFromRunningDB will retrieve db config for main cluster nodes and all sandbox names from the main cluster, +// then fetch sandbox from each sandbox one by one separately and update the vdb object. This will return accurate cluster +// status if the main cluster is UP. +func (vcc VClusterCommands) getDeepVDBFromRunningDB(vdb *VCoordinationDatabase, options *DatabaseOptions) error { + // Fetch vdb from main cluster + mainVdb := makeVCoordinationDatabase() + mainErr := vcc.getVDBFromRunningDBImpl(&mainVdb, options, false /*allow use http result from sandbox nodes*/, util.MainClusterSandbox, + false /*update node state by sending http request to each node*/) + if mainErr != nil { + vcc.Log.Info("failed to get vdb info from main cluster, database could be down. Attempting to connect to sandboxes") + return vcc.getVDBFromRunningDBImpl(vdb, options, true /*allow use http result from sandbox nodes*/, AnySandbox, + false /*update node state by sending http request to each node*/) + } + // update vdb with main cluster info and retrieve all sandbox names + vdb.setMainCluster(&mainVdb) + // If we reach here, the main cluster is UP and we can fetch accurate vdb info from each sandbox separately + for _, sandbox := range vdb.AllSandboxes { + sandVdb := makeVCoordinationDatabase() + sandErr := vcc.getVDBFromRunningDBImpl(&sandVdb, options, true /*allow use http result from sandbox nodes*/, sandbox, + false /*update node state by sending http request to each node*/) + if sandErr != nil { + vcc.Log.Info("failed to get vdb info from sandbox %s", sandbox) + } else { + // update vdb with sandbox info + vdb.updateSandboxNodeInfo(&sandVdb, sandbox) + } + } + return nil +} + // getVDBFromRunningDBIncludeSandbox will retrieve db configurations from a sandboxed host by calling https endpoints of a running db func (vcc VClusterCommands) getVDBFromRunningDBIncludeSandbox(vdb *VCoordinationDatabase, options *DatabaseOptions, sandbox string) error { return vcc.getVDBFromRunningDBImpl(vdb, options, true /*allow use http result from sandbox nodes*/, sandbox, 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_create_tls_authentication_op.go b/vclusterops/https_create_tls_authentication_op.go new file mode 100644 index 0000000..331d39d --- /dev/null +++ b/vclusterops/https_create_tls_authentication_op.go @@ -0,0 +1,114 @@ +/* + (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 httpsCreateTLSAuthOp struct { + opBase + opHTTPSBase + authName string + authHosts string +} + +func makeHTTPSCreateTLSAuthOp(hosts []string, useHTTPPassword bool, userName string, httpsPassword *string, + authName, authHosts string) (httpsCreateTLSAuthOp, error) { + op := httpsCreateTLSAuthOp{} + op.name = "HTTPSCreateTLSAuthOp" + op.description = "Create TLS Authentication method" + op.authName = authName + op.authHosts = authHosts + // this op is a cluster-wide op, should be sent to only one host + 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 + } + return op, nil +} + +func (op *httpsCreateTLSAuthOp) setupClusterHTTPRequest(hosts []string) error { + for _, host := range hosts { + httpRequest := hostHTTPRequest{} + httpRequest.Method = PostMethod + httpRequest.buildHTTPSEndpoint(util.TLSAuthEndpoint + op.authName) + httpRequest.QueryParams = map[string]string{"host": op.authHosts} + if op.useHTTPPassword { + httpRequest.Password = op.httpsPassword + httpRequest.Username = op.userName + } + op.clusterHTTPRequest.RequestCollection[host] = httpRequest + } + + return nil +} + +func (op *httpsCreateTLSAuthOp) prepare(execContext *opEngineExecContext) error { + execContext.dispatcher.setup(op.hosts) + + return op.setupClusterHTTPRequest(op.hosts) +} + +func (op *httpsCreateTLSAuthOp) execute(execContext *opEngineExecContext) error { + if err := op.runExecute(execContext); err != nil { + return err + } + + return op.processResult(execContext) +} + +func (op *httpsCreateTLSAuthOp) processResult(_ *opEngineExecContext) error { + var allErrs error + + // should only send request to one host as creating authentication method is a cluster-wide op + // using for-loop here for accommodating potential future cases for sandboxes + for host, result := range op.clusterHTTPRequest.ResultCollection { + op.logResponse(host, result) + err := result.getError(host, op.name) + if err != nil { + allErrs = errors.Join(allErrs, err) + continue + } + + // Example successful response object: + /* + { + "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 *httpsCreateTLSAuthOp) finalize(_ *opEngineExecContext) error { + return nil +} diff --git a/vclusterops/https_get_up_nodes_op.go b/vclusterops/https_get_up_nodes_op.go index cdaec9d..ded3a6e 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 := "" @@ -283,7 +289,16 @@ func (op *httpsGetUpNodesOp) validateHosts(nodesStates nodesStateInfo) error { unexpectedDBName = node.Database dbUnexpected = true } - dbHosts = append(dbHosts, node.Address) + // If we want to stop a specific db group(sandbox/main cluster), we only need to consider + // hosts from that specific db group + if op.mainCluster || (op.sandbox != util.MainClusterSandbox) && op.cmdType != StopSubclusterCmd { + if (op.mainCluster && node.Sandbox == util.MainClusterSandbox) || + (op.sandbox != util.MainClusterSandbox && op.sandbox == node.Sandbox) { + dbHosts = append(dbHosts, node.Address) + } + } else { + dbHosts = append(dbHosts, node.Address) + } } // when db name does not match, we throw an error if dbUnexpected { @@ -310,7 +325,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 +348,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_grant_authentication_op.go b/vclusterops/https_grant_authentication_op.go new file mode 100644 index 0000000..96cb453 --- /dev/null +++ b/vclusterops/https_grant_authentication_op.go @@ -0,0 +1,113 @@ +/* + (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 httpsGrantTLSAuthOp struct { + opBase + opHTTPSBase + authName string + grantee string +} + +func makeHTTPSGrantTLSAuthOp(hosts []string, useHTTPPassword bool, userName string, httpsPassword *string, + authName, grantee string) (httpsGrantTLSAuthOp, error) { + op := httpsGrantTLSAuthOp{} + op.name = "HTTPSGrantTLSAuthOp" + op.description = "Grant TLS Authentication method to users" + op.authName = authName + op.grantee = grantee + + 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 + } + return op, nil +} + +func (op *httpsGrantTLSAuthOp) setupClusterHTTPRequest(hosts []string) error { + for _, host := range hosts { + httpRequest := hostHTTPRequest{} + httpRequest.Method = PostMethod + httpRequest.buildHTTPSEndpoint(util.TLSAuthEndpoint + op.authName + "/grant") + // the grantee usually is 'public' + httpRequest.QueryParams = map[string]string{"grantee": op.grantee} + if op.useHTTPPassword { + httpRequest.Password = op.httpsPassword + httpRequest.Username = op.userName + } + op.clusterHTTPRequest.RequestCollection[host] = httpRequest + } + + return nil +} + +func (op *httpsGrantTLSAuthOp) prepare(execContext *opEngineExecContext) error { + execContext.dispatcher.setup(op.hosts) + + return op.setupClusterHTTPRequest(op.hosts) +} + +func (op *httpsGrantTLSAuthOp) execute(execContext *opEngineExecContext) error { + if err := op.runExecute(execContext); err != nil { + return err + } + + return op.processResult(execContext) +} + +func (op *httpsGrantTLSAuthOp) processResult(_ *opEngineExecContext) error { + var allErrs error + + // should only send request to one host as grant authentication method is a cluster-wide op + for host, result := range op.clusterHTTPRequest.ResultCollection { + op.logResponse(host, result) + err := result.getError(host, op.name) + if err != nil { + allErrs = errors.Join(allErrs, err) + continue + } + // Example successful response object: + /* + { + "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 *httpsGrantTLSAuthOp) finalize(_ *opEngineExecContext) error { + return nil +} 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/nma_read_catalog_editor_op.go b/vclusterops/nma_read_catalog_editor_op.go index 5e90ccb..ada99d5 100644 --- a/vclusterops/nma_read_catalog_editor_op.go +++ b/vclusterops/nma_read_catalog_editor_op.go @@ -56,6 +56,14 @@ func makeNMAReadCatalogEditorOpWithInitiator( return op, nil } +// under sandbox mode, each sandbox are using different files for vcluster catalog, +// when accessing catalog editor for a sandbox, we need to feed sandbox as the op's parameter +func makeNMAReadCatalogEditorOpWithSandbox(vdb *VCoordinationDatabase, sandbox string) (nmaReadCatalogEditorOp, error) { + op, err := makeNMAReadCatalogEditorOpWithInitiator([]string{}, vdb) + op.sandbox = sandbox + return op, err +} + // makeNMAReadCatalogEditorOp creates an op to read catalog editor info. func makeNMAReadCatalogEditorOp(vdb *VCoordinationDatabase) (nmaReadCatalogEditorOp, error) { return makeNMAReadCatalogEditorOpWithInitiator([]string{}, vdb) diff --git a/vclusterops/re_ip.go b/vclusterops/re_ip.go index 07635b0..459ee2b 100644 --- a/vclusterops/re_ip.go +++ b/vclusterops/re_ip.go @@ -29,8 +29,8 @@ type VReIPOptions struct { DatabaseOptions // re-ip list - ReIPList []ReIPInfo - + ReIPList []ReIPInfo + SandboxName string // sandbox name or empty string for main cluster, all nodes must in same sandbox for one re_ip action /* hidden option */ // whether trim re-ip list based on the catalog info @@ -45,7 +45,7 @@ func VReIPFactory() VReIPOptions { // set default values to the params options.setDefaultValues() options.TrimReIPList = false - + options.SandboxName = util.MainClusterSandbox return options } @@ -169,7 +169,7 @@ func (vcc VClusterCommands) VReIP(options *VReIPOptions) error { const warningMsg = " for an Eon database, re_ip after revive_db could fail " + util.DBInfo if options.CommunalStorageLocation != "" { - vdb, e := options.getVDBWhenDBIsDown(vcc) + vdb, e := options.getVDBFromSandboxWhenDBIsDown(vcc, options.SandboxName) if e != nil { // show a warning message if we cannot get VDB from a down database vcc.Log.PrintWarning(util.CommStorageFail + warningMsg) @@ -193,9 +193,18 @@ func (vcc VClusterCommands) VReIP(options *VReIPOptions) error { 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 re-ip: %w", runError) + if options.SandboxName == util.MainClusterSandbox { + vcc.LogInfo("Re-IP the main cluster") + runError := clusterOpEngine.run(vcc.Log) + if runError != nil { + return fmt.Errorf("fail to re-ip: %w", runError) + } + } else { + vcc.LogInfo("Re-IP the sandbox %s", options.SandboxName) + runError := clusterOpEngine.runInSandbox(vcc.Log, pVDB, options.SandboxName) + if runError != nil { + return fmt.Errorf("fail to re-ip: %w", runError) + } } return nil @@ -226,8 +235,13 @@ func (vcc VClusterCommands) produceReIPInstructions(options *VReIPOptions, vdb * instructions = append(instructions, &nmaHealthOp) if options.CheckDBRunning { - checkDBRunningOp, err := makeHTTPSCheckRunningDBOp(hosts, - options.usePassword, options.UserName, options.Password, ReIP) + sandbox := options.SandboxName + mainCluster := false + if sandbox == util.MainClusterSandbox { + mainCluster = true + } + checkDBRunningOp, err := makeHTTPSCheckRunningDBWithSandboxOp(hosts, + options.usePassword, options.UserName, sandbox, mainCluster, options.Password, ReIP) if err != nil { return instructions, err } @@ -250,7 +264,7 @@ func (vcc VClusterCommands) produceReIPInstructions(options *VReIPOptions, vdb * nmaGetNodesInfoOp := makeNMAGetNodesInfoOp(options.Hosts, options.DBName, options.CatalogPrefix, false /* report all errors */, vdb) // read catalog editor to get hosts with latest catalog - nmaReadCatEdOp, err := makeNMAReadCatalogEditorOp(vdb) + nmaReadCatEdOp, err := makeNMAReadCatalogEditorOpWithSandbox(vdb, options.SandboxName) if err != nil { return instructions, err } @@ -263,7 +277,7 @@ func (vcc VClusterCommands) produceReIPInstructions(options *VReIPOptions, vdb * *vdbWithPrimaryNodes = *vdb vdbWithPrimaryNodes.filterPrimaryNodes() // read catalog editor to get hosts with latest catalog - nmaReadCatEdOp, err := makeNMAReadCatalogEditorOp(vdbWithPrimaryNodes) + nmaReadCatEdOp, err := makeNMAReadCatalogEditorOpWithSandbox(vdbWithPrimaryNodes, options.SandboxName) if err != nil { return instructions, err } diff --git a/vclusterops/replication.go b/vclusterops/replication.go index b08a197..2cc2d5c 100644 --- a/vclusterops/replication.go +++ b/vclusterops/replication.go @@ -98,8 +98,8 @@ func (options *VReplicationDatabaseOptions) validateExtraOptions() error { func (options *VReplicationDatabaseOptions) validateFineGrainedReplicationOptions() error { if options.TableOrSchemaName != "" { err := util.ValidateQualifiedObjectNamePattern(options.TableOrSchemaName, false) - if err != nil { - return err + if err != nil && strings.HasPrefix(err.Error(), "invalid pattern") { + return fmt.Errorf("pattern %s not allowed in --table-or-schema-name. HINT: use --include-pattern", options.TableOrSchemaName) } } 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/start_node.go b/vclusterops/start_node.go index f19976a..79e27f4 100644 --- a/vclusterops/start_node.go +++ b/vclusterops/start_node.go @@ -28,7 +28,7 @@ import ( type VStartNodesOptions struct { // basic db info DatabaseOptions - // A set of nodes(nodename - host) that we want to start in the database + // A set of nodes (nodename - host) that we want to start in the database Nodes map[string]string // timeout for polling nodes that we want to start in httpsPollNodeStateOp StatePollingTimeout int @@ -62,6 +62,9 @@ type VStartNodesInfo struct { SerialReIP bool // Number of up hosts upHostCount int + // whether allow start unbound nodes individually + // currently, we only allow the Kubernetes operator to do so + DoAllowStartUnboundNodes bool } func VStartNodesOptionsFactory() VStartNodesOptions { @@ -178,7 +181,7 @@ func (vcc VClusterCommands) preStartNodeCheck(options *VStartNodesOptions, vdb * // retrieve database information to execute the command so we do not always rely on some user input // if VStartNodes is called from VStartSubcluster, we can reuse the vdb from VStartSubcluster if options.vdb == nil { - err := vcc.getVDBFromRunningDBIncludeSandbox(vdb, &options.DatabaseOptions, AnySandbox) + err := vcc.getDeepVDBFromRunningDB(vdb, &options.DatabaseOptions) if err != nil { return err } @@ -206,6 +209,23 @@ func (vcc VClusterCommands) preStartNodeCheck(options *VStartNodesOptions, vdb * } return errors.Join(err, fmt.Errorf("hint: make sure there is at least one UP node in the database")) } + + // to avoid problems in the catalog, we only allow starting unbound nodes using start_subcluster + // or when startNodeInfo.DoAllowStartUnboundNodes is true (which should only be set by the Kubernetes operator) + var isStartingUnboundNodes bool + for _, vnode := range vdb.UnboundNodes { + _, toStart := options.Nodes[vnode.Name] + if toStart { + isStartingUnboundNodes = true + break + } + } + + if isStartingUnboundNodes && (!startNodeInfo.isStartSc && !startNodeInfo.DoAllowStartUnboundNodes) { + return errors.New("cannot directly start unbound nodes. " + + "Please use start_subclusters to start unbound subclusters with new IP addresses") + } + return nil } @@ -237,8 +257,8 @@ func (vcc VClusterCommands) VStartNodes(options *VStartNodesOptions) error { vdb = *options.vdb startNodeInfo.isStartSc = true } - hostNodeNameMap := make(map[string]string) + hostNodeNameMap := make(map[string]string) err = vcc.preStartNodeCheck(options, &vdb, hostNodeNameMap, startNodeInfo) if err != nil { return err diff --git a/vclusterops/stop_db.go b/vclusterops/stop_db.go index 3b575b5..cffb217 100644 --- a/vclusterops/stop_db.go +++ b/vclusterops/stop_db.go @@ -140,7 +140,13 @@ func (vcc VClusterCommands) VStopDatabase(options *VStopDatabaseOptions) error { // get vdb and check requirements vdb := makeVCoordinationDatabase() - err = vcc.getVDBFromRunningDBIncludeSandbox(&vdb, &options.DatabaseOptions, AnySandbox) + if options.MainCluster { + vcc.Log.Info("getting vdb info from main cluster") + err = vcc.getVDBFromRunningDB(&vdb, &options.DatabaseOptions) + } else { + vcc.Log.Info("getting vdb info for sandbox") + err = vcc.getDeepVDBFromRunningDB(&vdb, &options.DatabaseOptions) + } if err != nil { vcc.LogError(err, "failed to get vdb from running db") } else { @@ -150,12 +156,23 @@ func (vcc VClusterCommands) VStopDatabase(options *VStopDatabaseOptions) error { return err } } + filteredHosts := []string{} + for _, h := range options.Hosts { + if vnode, exists := vdb.HostNodeMap[h]; exists { + if (options.MainCluster && vnode.Sandbox == util.MainClusterSandbox) || + (options.SandboxName == vnode.Sandbox && options.SandboxName != util.MainClusterSandbox) { + filteredHosts = append(filteredHosts, h) + } + } + } + if len(filteredHosts) > 0 { + options.Hosts = filteredHosts + } instructions, err := vcc.produceStopDBInstructions(options) if err != nil { return fmt.Errorf("fail to production instructions: %w", err) } - // Create a VClusterOpEngine, and add certs to the engine clusterOpEngine := makeClusterOpEngine(instructions, options) @@ -196,7 +213,6 @@ func (vcc *VClusterCommands) produceStopDBInstructions(options *VStopDatabaseOpt return instructions, err } instructions = append(instructions, &httpsGetUpNodesOp) - if options.IsEon { httpsSyncCatalogOp, e := makeHTTPSSyncCatalogOpWithoutHosts(usePassword, options.UserName, options.Password, StopDBSyncCat) if e != nil { diff --git a/vclusterops/stop_node.go b/vclusterops/stop_node.go index 5d70932..572449b 100644 --- a/vclusterops/stop_node.go +++ b/vclusterops/stop_node.go @@ -102,7 +102,7 @@ func (vcc VClusterCommands) VStopNode(options *VStopNodeOptions) error { return err } - err = vcc.getVDBFromRunningDB(&vdb, &options.DatabaseOptions) + err = vcc.getDeepVDBFromRunningDB(&vdb, &options.DatabaseOptions) if err != nil { return err } diff --git a/vclusterops/unsandbox.go b/vclusterops/unsandbox.go index 906bb0b..24bc743 100644 --- a/vclusterops/unsandbox.go +++ b/vclusterops/unsandbox.go @@ -17,6 +17,7 @@ package vclusterops import ( "fmt" + "slices" "github.com/vertica/vcluster/rfc7807" "github.com/vertica/vcluster/vclusterops/util" @@ -30,8 +31,6 @@ type VUnsandboxOptions struct { SCRawHosts []string // if restart the subcluster after unsandboxing it, the default value of it is true RestartSC bool - // if any node in the target subcluster is up. This is for internal use only. - hasUpNodeInSC bool // The expected node names with their IPs in the subcluster, the user of vclusterOps need // to make sure the provided values are correct. This option will be used to do re-ip in // the main cluster. @@ -117,62 +116,213 @@ func (e *SubclusterNotSandboxedError) Error() string { return fmt.Sprintf(`cannot unsandbox a regular subcluster [%s]`, e.SCName) } +// info to be used while populating vdb +type ProcessedVDBInfo struct { + // expect to fill the fields with info obtained from the main cluster + MainClusterUpHosts []string // all UP hosts in the main cluster + MainPrimaryUpHost string // primary UP host in the main cluster + mainClusterNodeNameAddressMap map[string]string // NodeName to Address map for Main cluster nodes, this will be used to re-ip + sandAddrsFromMain []string // all sandboxed IPs as collected from main cluster vdb + sandScAddrsFromMain []string // all sandboxed subcluster IPs as collected from main cluster vdb + SandboxName string // name of sandbox which contains the subcluster to be unsandboxed + ScFound bool // is subcluster found in vdb + + // expect to fill the fields with info obtained from the sandbox + UpSandboxHost string // UP host in the sandbox + SandboxedHosts []string // All hosts in the sandbox which contains the subcluster to be unsandboxed + SandboxedSubclusterHosts []string // All hosts in the subcluster to be unsandboxed + SandboxedNodeNameAddressMap map[string]string // NodeName to Address map for sandboxed nodes, this will be used to re-ip + upSCHosts []string // subcluster hosts that are UP + hasUpNodeInSC bool // if any node in the target subcluster is up. This is for internal use only. +} + // unsandboxPreCheck will build a list of instructions to perform // unsandbox_subcluster pre-checks // // The generated instructions will later perform the following operations necessary // for a successful unsandbox_subcluster -// - Get cluster and nodes info (check if the DB is Eon) -// - Get the subcluster info (check if the target subcluster is sandboxed) -func (vcc *VClusterCommands) unsandboxPreCheck(vdb *VCoordinationDatabase, options *VUnsandboxOptions) error { +// - Get cluster and nodes info from Main cluster (check if the DB is Eon) +// - Get node info from sandboxed nodes +// - validate if the subcluster to be unsandboxed exists and is sandboxed +// - run re-ip on main cluster (with true ips of sandbox hosts) and on sandbox (with true ips of main cluster hosts) +// This avoids issues when unsandboxed cluster joins the main cluster +func (vcc *VClusterCommands) unsandboxPreCheck(vdb *VCoordinationDatabase, + options *VUnsandboxOptions, + vdbInfo *ProcessedVDBInfo) error { + // Get main cluster vdb + err := vcc.getMainClusterVDB(vdb, options) + if err != nil { + return err + } + + // Process main cluster nodes + err = vcc.processMainClusterNodes(vdb, options, vdbInfo) + if err != nil { + return err + } + + if !vdbInfo.ScFound { + return vcc.handleSubclusterNotFound(options) + } + + // Backup original main cluster HostNodeMap (deep copy) + originalHostNodeMap := util.CloneMap(vdb.HostNodeMap, CloneVCoordinationNode) + + // Delete sandbox vals from vdb + vdb.HostNodeMap = util.DeleteKeysFromMap(vdb.HostNodeMap, vdbInfo.sandAddrsFromMain) + + // Populate sandbox details + err = vcc.updateSandboxDetails(vdb, options, vdbInfo) + if err != nil { + vcc.Log.PrintWarning("Failed to retrieve sandbox details for '%s', "+ + "main cluster might need to re-ip if sandbox host IPs have changed", vdbInfo.SandboxName) + vdb.HostNodeMap = originalHostNodeMap + } + + // run re-ip on both of main cluster and the sandbox. + err = vcc.reIPNodes(options, vdbInfo.UpSandboxHost, vdbInfo.MainPrimaryUpHost, + vdbInfo.SandboxedNodeNameAddressMap, vdbInfo.mainClusterNodeNameAddressMap) + if err != nil { + return err + } + // Update options and finalize configuration + options.Hosts = vdbInfo.MainClusterUpHosts + return vcc.setClusterHosts(options, vdbInfo) +} + +func (vcc *VClusterCommands) handleSubclusterNotFound(options *VUnsandboxOptions) error { + vcc.Log.PrintError("Subcluster '%s' does not exist", options.SCName) + rfcErr := rfc7807.New(rfc7807.SubclusterNotFound).WithHost(options.Hosts[0]) + return rfcErr +} + +func (vcc *VClusterCommands) getMainClusterVDB(vdb *VCoordinationDatabase, options *VUnsandboxOptions) error { err := vcc.getVDBFromMainRunningDBContainsSandbox(vdb, &options.DatabaseOptions) if err != nil { return err } if !vdb.IsEon { - return fmt.Errorf(`cannot unsandbox subclusters for an enterprise database '%s'`, - options.DBName) + return fmt.Errorf("cannot unsandbox subclusters for an enterprise database '%s'", options.DBName) } + return nil +} - scFound := false - var sandboxedHosts []string +// update processed vdb info object and vdb with the sandbox details +func (vcc *VClusterCommands) updateSandboxDetails( + vdb *VCoordinationDatabase, + options *VUnsandboxOptions, + info *ProcessedVDBInfo, +) error { + info.SandboxedNodeNameAddressMap = make(map[string]string) + sandVdb := makeVCoordinationDatabase() - upHosts := []string{} - for _, vnode := range vdb.HostNodeMap { - if !scFound && vnode.Subcluster == options.SCName { - scFound = true + err := vcc.getVDBFromRunningDBIncludeSandbox(&sandVdb, &options.DatabaseOptions, info.SandboxName) + if err != nil { + return err + } + // fill in the remainder of the fields of info not filled by main cluster + for _, vnode := range sandVdb.HostNodeMap { + if vnode.Sandbox == info.SandboxName { + vdb.HostNodeMap[vnode.Address] = vnode + info.SandboxedHosts = append(info.SandboxedHosts, vnode.Address) + } + if vnode.State == util.NodeUpState { + info.hasUpNodeInSC = true + info.upSCHosts = append(info.upSCHosts, vnode.Address) + if vnode.IsPrimary { + info.UpSandboxHost = vnode.Address + } + } + if vnode.Subcluster == options.SCName { + info.SandboxedSubclusterHosts = append(info.SandboxedSubclusterHosts, vnode.Address) + info.SandboxedNodeNameAddressMap[vnode.Name] = vnode.Address } + } + return nil +} - if vnode.State != util.NodeDownState { - upHosts = append(upHosts, vnode.Address) +func (vcc *VClusterCommands) processMainClusterNodes( + vdb *VCoordinationDatabase, + options *VUnsandboxOptions, + info *ProcessedVDBInfo, +) error { + info.mainClusterNodeNameAddressMap = make(map[string]string) + info.ScFound = false + + for _, vnode := range vdb.HostNodeMap { + // Collect UP hosts and primary host + if vnode.Sandbox == util.MainClusterSandbox { + // Populate main cluster node map + info.mainClusterNodeNameAddressMap[vnode.Name] = vnode.Address + if vnode.State == util.NodeUpState { + info.MainClusterUpHosts = append(info.MainClusterUpHosts, vnode.Address) + if vnode.IsPrimary { + info.MainPrimaryUpHost = vnode.Address + } + } } + + // Check for the specific subcluster if vnode.Subcluster == options.SCName { - // if the subcluster is not sandboxed, return error immediately + info.ScFound = true + info.sandScAddrsFromMain = append(info.sandScAddrsFromMain, vnode.Address) if vnode.Sandbox == "" { return &SubclusterNotSandboxedError{SCName: options.SCName} } - sandboxedHosts = append(sandboxedHosts, vnode.Address) - // when the node state is not "DOWN" ("UP" or "UNKNOWN"), we consider - // the node is running - if vnode.State != util.NodeDownState { - options.hasUpNodeInSC = true - } + info.SandboxName = vnode.Sandbox } } - // change hosts in options to all up hosts so the user can only provide hosts in main cluster - options.Hosts = upHosts + // Update sandbox node details + fetchAllSandHosts(vdb, info) + return nil +} - if !scFound { - vcc.Log.PrintError(`subcluster '%s' does not exist`, options.SCName) - rfcErr := rfc7807.New(rfc7807.SubclusterNotFound).WithHost(options.Hosts[0]) - return rfcErr +func fetchAllSandHosts(vdb *VCoordinationDatabase, info *ProcessedVDBInfo) { + for _, vnode := range vdb.HostNodeMap { + if vnode.Sandbox == info.SandboxName { + info.sandAddrsFromMain = append(info.sandAddrsFromMain, vnode.Address) + } } - - mainClusterHost := util.SliceDiff(options.Hosts, sandboxedHosts) - if len(mainClusterHost) == 0 { +} +func (vcc *VClusterCommands) setClusterHosts(options *VUnsandboxOptions, info *ProcessedVDBInfo) error { + options.SCHosts = info.sandScAddrsFromMain + if len(info.SandboxedSubclusterHosts) > 0 { + options.SCHosts = info.SandboxedSubclusterHosts + } + if len(info.MainClusterUpHosts) == 0 { return fmt.Errorf(`require at least one UP host outside of the sandbox subcluster '%s'in the input host list`, options.SCName) } - options.SCHosts = sandboxedHosts + return nil +} + +func (vcc *VClusterCommands) reIPNodes(options *VUnsandboxOptions, upSandboxHost, mainPrimaryUpHost string, + sandboxedNodeNameAddressMap, mainClusterNodeNameAddressMap map[string]string) error { + // Skip re-ip if NodeNameAddressMap and PrimaryUpHost are already set + if len(options.NodeNameAddressMap) > 0 && options.PrimaryUpHost != "" { + return nil + } + + // Handle re-ip on sandbox + if upSandboxHost == "" { + vcc.Log.PrintWarning("Skipping re-ip step on sandboxes as there are no UP nodes in the target sandbox.") + } else { + err := vcc.reIP(&options.DatabaseOptions, options.SCName, mainPrimaryUpHost, sandboxedNodeNameAddressMap, true /*reload spread*/) + if err != nil { + return fmt.Errorf("failed re-ip on sandbox: %w", err) + } + options.NodeNameAddressMap = sandboxedNodeNameAddressMap + } + + // Handle reIP on main cluster + if mainPrimaryUpHost == "" { + vcc.Log.PrintWarning("Skipping re-ip step on main cluster as there are no primary UP nodes in the main cluster.") + } else { + err := vcc.reIP(&options.DatabaseOptions, "main cluster", upSandboxHost, mainClusterNodeNameAddressMap, true /*reload spread*/) + if err != nil { + return fmt.Errorf("failed re-ip on main cluster: %w", err) + } + } + return nil } @@ -193,7 +343,7 @@ func (vcc *VClusterCommands) unsandboxPreCheck(vdb *VCoordinationDatabase, optio // 2. get start commands from UP main cluster node // 3. run startup commands for unsandboxed nodes // 4. Poll for started nodes to be UP -func (vcc *VClusterCommands) produceUnsandboxSCInstructions(options *VUnsandboxOptions) ([]clusterOp, error) { +func (vcc *VClusterCommands) produceUnsandboxSCInstructions(options *VUnsandboxOptions, info *ProcessedVDBInfo) ([]clusterOp, error) { var instructions []clusterOp // when password is specified, we will use username/password to call https endpoints @@ -212,7 +362,10 @@ func (vcc *VClusterCommands) produceUnsandboxSCInstructions(options *VUnsandboxO instructions = append(instructions, &nmaHealthOp) // Get all up nodes - httpsGetUpNodesOp, err := makeHTTPSGetUpScNodesOp(options.DBName, options.Hosts, + // options.Hosts has main cluster hosts and info.upSCHosts has UP Sandbox hosts, both of them + // are used to update the execContext and used later in various unsandboxing related Ops + allUpHosts := slices.Concat(options.Hosts, info.upSCHosts) + httpsGetUpNodesOp, err := makeHTTPSGetUpScNodesOp(options.DBName, allUpHosts, usePassword, username, options.Password, UnsandboxSCCmd, options.SCName) if err != nil { return instructions, err @@ -225,7 +378,7 @@ func (vcc *VClusterCommands) produceUnsandboxSCInstructions(options *VUnsandboxO scHosts = append(scHosts, host) scNodeNames = append(scNodeNames, nodeName) } - if options.hasUpNodeInSC { + if info.hasUpNodeInSC { // Stop the nodes in the subcluster that is to be unsandboxed httpsStopNodeOp, e := makeHTTPSStopNodeOp(scHosts, scNodeNames, usePassword, username, options.Password, nil) @@ -305,12 +458,13 @@ func (options *VUnsandboxOptions) runCommand(vcc VClusterCommands) error { } vdb := makeVCoordinationDatabase() - err := vcc.unsandboxPreCheck(&vdb, options) + var vdbInfo ProcessedVDBInfo + err := vcc.unsandboxPreCheck(&vdb, options, &vdbInfo) if err != nil { return err } // make instructions - instructions, err := vcc.produceUnsandboxSCInstructions(options) + instructions, err := vcc.produceUnsandboxSCInstructions(options, &vdbInfo) if err != nil { return fmt.Errorf("fail to produce instructions, %w", err) } 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/defaults.go b/vclusterops/util/defaults.go index 8362aac..047fc8c 100644 --- a/vclusterops/util/defaults.go +++ b/vclusterops/util/defaults.go @@ -47,4 +47,14 @@ const ( MainClusterSandbox = "" ) +// TLS authentication related consts +const ( + // IPv4 defaults + DefaultIPv4AuthName = "vcluster_mtls_v4" + DefaultIPv4AuthHosts = "0.0.0.0/0" + // IPv6 defaults + DefaultIPv6AuthName = "vcluster_mtls_v6" + DefaultIPv6AuthHosts = "::/0" +) + var RestartPolicyList = []string{"never", DefaultRestartPolicy, "always"} diff --git a/vclusterops/util/util.go b/vclusterops/util/util.go index 12d2f32..fbd5dda 100644 --- a/vclusterops/util/util.go +++ b/vclusterops/util/util.go @@ -67,6 +67,8 @@ const ( NodesEndpoint = "nodes/" DropEndpoint = "/drop" ArchiveEndpoint = "archives" + LicenseEndpoint = "license" + TLSAuthEndpoint = "authentication/tls/" ) const ( @@ -159,6 +161,23 @@ func CheckAllEmptyOrNonEmpty(vars ...string) bool { return allEmpty || allNonEmpty } +// delete keys in the given iterable from the given map +func DeleteKeysFromMap[K comparable, V any](m map[K]V, keys []K) map[K]V { + for _, key := range keys { + delete(m, key) + } + return m +} + +// Creates and returns a deep copy of the given map +func CloneMap[K comparable, V any](original map[K]V, cloneValue func(V) V) map[K]V { + clone := make(map[K]V, len(original)) + for key, value := range original { + clone[key] = cloneValue(value) + } + return clone +} + // calculate array diff: m-n func SliceDiff[K comparable](m, n []K) []K { nSet := make(map[K]struct{}, len(n)) diff --git a/vclusterops/vcluster_database_options.go b/vclusterops/vcluster_database_options.go index 1cb38e6..58d0f26 100644 --- a/vclusterops/vcluster_database_options.go +++ b/vclusterops/vcluster_database_options.go @@ -288,8 +288,10 @@ 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 -func (opt *DatabaseOptions) getVDBWhenDBIsDown(vcc VClusterCommands) (vdb VCoordinationDatabase, err error) { +// 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. @@ -308,7 +310,7 @@ func (opt *DatabaseOptions) getVDBWhenDBIsDown(vcc VClusterCommands) (vdb VCoord var instructions1 []clusterOp nmaHealthOp := makeNMAHealthOp(opt.Hosts) nmaGetNodesInfoOp := makeNMAGetNodesInfoOp(opt.Hosts, opt.DBName, opt.CatalogPrefix, - false /* report all errors */, &vdb1) + true /* ignore internal error */, &vdb1) instructions1 = append(instructions1, &nmaHealthOp, &nmaGetNodesInfoOp, @@ -324,7 +326,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 { @@ -341,7 +343,7 @@ func (opt *DatabaseOptions) getVDBWhenDBIsDown(vcc VClusterCommands) (vdb VCoord // step 3: build vdb for input hosts using node names from step 1 and node details from step 2 // this step can map input hosts with node details - vdb.HostList = vdb1.HostList + vdb.HostList = vdb2.HostList vdb.HostNodeMap = makeVHostNodeMap() nodeNameVNodeMap := make(map[string]*VCoordinationNode) for _, vnode2 := range vdb2.HostNodeMap { @@ -358,8 +360,6 @@ func (opt *DatabaseOptions) getVDBWhenDBIsDown(vcc VClusterCommands) (vdb VCoord // the nodes' addresses without syncing the change to cluster_config.json. vnode.Address = h1 vdb.HostNodeMap[h1] = vnode - } else { - return vdb, fmt.Errorf("node name %s is not found in %s", nodeName, descriptionFileName) } } return vdb, nil