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 {