X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/bf93ae69e8748f32a0c403ddf494a89e945f3201..fb44c6dbca84285813fd3278b14687ddc41de678:/lib/client/gnt_cluster.py?ds=sidebyside diff --git a/lib/client/gnt_cluster.py b/lib/client/gnt_cluster.py index c5eebf7..f3ada54 100644 --- a/lib/client/gnt_cluster.py +++ b/lib/client/gnt_cluster.py @@ -20,7 +20,7 @@ """Cluster related commands""" -# pylint: disable-msg=W0401,W0613,W0614,C0103 +# pylint: disable=W0401,W0613,W0614,C0103 # W0401: Wildcard import ganeti.cli # W0613: Unused argument, since all functions follow the same API # W0614: Unused import %s from wildcard import (since we need cli) @@ -223,6 +223,32 @@ def RenameCluster(opts, args): return 0 +def ActivateMasterIp(opts, args): + """Activates the master IP. + + """ + op = opcodes.OpClusterActivateMasterIp() + SubmitOpCode(op) + return 0 + + +def DeactivateMasterIp(opts, args): + """Deactivates the master IP. + + """ + if not opts.confirm: + usertext = ("This will disable the master IP. All the open connections to" + " the master IP will be closed. To reach the master you will" + " need to use its node IP." + " Continue?") + if not AskUser(usertext): + return 1 + + op = opcodes.OpClusterDeactivateMasterIp() + SubmitOpCode(op) + return 0 + + def RedistributeConfig(opts, args): """Forces push of the cluster configuration. @@ -363,6 +389,7 @@ def ShowClusterConfig(opts, args): ToStdout(" - default instance allocator: %s", result["default_iallocator"]) ToStdout(" - primary ip version: %d", result["primary_ip_version"]) ToStdout(" - preallocation wipe disks: %s", result["prealloc_wipe_disks"]) + ToStdout(" - OS search path: %s", utils.CommaJoin(constants.OS_SEARCH_PATH)) ToStdout("Default node parameters:") _PrintGroupedParams(result["ndparams"], roman=opts.roman_integers) @@ -397,7 +424,8 @@ def ClusterCopyFile(opts, args): cluster_name = cl.QueryConfigValues(["cluster_name"])[0] results = GetOnlineNodes(nodes=opts.nodes, cl=cl, filter_master=True, - secondary_ips=opts.use_replication_network) + secondary_ips=opts.use_replication_network, + nodegroup=opts.nodegroup) srun = ssh.SshRunner(cluster_name=cluster_name) for node in results: @@ -421,7 +449,7 @@ def RunClusterCommand(opts, args): command = " ".join(args) - nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl) + nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl, nodegroup=opts.nodegroup) cluster_name, master_node = cl.QueryConfigValues(["cluster_name", "master_node"]) @@ -453,33 +481,46 @@ def VerifyCluster(opts, args): @return: the desired exit code """ - simulate = opts.simulate_errors skip_checks = [] - # Verify cluster config. - op = opcodes.OpClusterVerifyConfig(verbose=opts.verbose, - error_codes=opts.error_codes, - debug_simulate_errors=simulate) - - success, all_groups = SubmitOpCode(op, opts=opts) - if opts.skip_nplusone_mem: skip_checks.append(constants.VERIFY_NPLUSONE_MEM) - jex = JobExecutor(opts=opts, verbose=False) + cl = GetClient() + + op = opcodes.OpClusterVerify(verbose=opts.verbose, + error_codes=opts.error_codes, + debug_simulate_errors=opts.simulate_errors, + skip_checks=skip_checks, + group_name=opts.nodegroup) + result = SubmitOpCode(op, cl=cl, opts=opts) + + # Keep track of submitted jobs + jex = JobExecutor(cl=cl, opts=opts) - for group in all_groups: - op = opcodes.OpClusterVerifyGroup(group_name=group, - skip_checks=skip_checks, - verbose=opts.verbose, - error_codes=opts.error_codes, - debug_simulate_errors=simulate) - jex.QueueJob('group ' + group, op) + for (status, job_id) in result[constants.JOB_IDS_KEY]: + jex.AddJobId(None, status, job_id) results = jex.GetResults() - success &= compat.all(r[1][0] for r in results) - return (not success and 1 or 0) + (bad_jobs, bad_results) = \ + map(len, + # Convert iterators to lists + map(list, + # Count errors + map(compat.partial(itertools.ifilterfalse, bool), + # Convert result to booleans in a tuple + zip(*((job_success, len(op_results) == 1 and op_results[0]) + for (job_success, op_results) in results))))) + + if bad_jobs == 0 and bad_results == 0: + rcode = constants.EXIT_SUCCESS + else: + rcode = constants.EXIT_FAILURE + if bad_jobs > 0: + ToStdout("%s job(s) failed while verifying the cluster.", bad_jobs) + + return rcode def VerifyDisks(opts, args): @@ -495,22 +536,30 @@ def VerifyDisks(opts, args): cl = GetClient() op = opcodes.OpClusterVerifyDisks() - result = SubmitOpCode(op, opts=opts, cl=cl) - if not isinstance(result, (list, tuple)) or len(result) != 3: - raise errors.ProgrammerError("Unknown result type for OpClusterVerifyDisks") - bad_nodes, instances, missing = result + result = SubmitOpCode(op, cl=cl, opts=opts) + + # Keep track of submitted jobs + jex = JobExecutor(cl=cl, opts=opts) + + for (status, job_id) in result[constants.JOB_IDS_KEY]: + jex.AddJobId(None, status, job_id) retcode = constants.EXIT_SUCCESS - if bad_nodes: + for (status, result) in jex.GetResults(): + if not status: + ToStdout("Job failed: %s", result) + continue + + ((bad_nodes, instances, missing), ) = result + for node, text in bad_nodes.items(): ToStdout("Error gathering data on node %s: %s", node, utils.SafeEncode(text[-400:])) - retcode |= 1 + retcode = constants.EXIT_FAILURE ToStdout("You need to fix these nodes first before fixing instances") - if instances: for iname in instances: if iname in missing: continue @@ -523,24 +572,24 @@ def VerifyDisks(opts, args): retcode |= nret ToStderr("Error activating disks for instance %s: %s", iname, msg) - if missing: - for iname, ival in missing.iteritems(): - all_missing = compat.all(x[0] in bad_nodes for x in ival) - if all_missing: - ToStdout("Instance %s cannot be verified as it lives on" - " broken nodes", iname) - else: - ToStdout("Instance %s has missing logical volumes:", iname) - ival.sort() - for node, vol in ival: - if node in bad_nodes: - ToStdout("\tbroken node %s /dev/%s", node, vol) - else: - ToStdout("\t%s /dev/%s", node, vol) - - ToStdout("You need to run replace or recreate disks for all the above" - " instances, if this message persist after fixing nodes.") - retcode |= 1 + if missing: + for iname, ival in missing.iteritems(): + all_missing = compat.all(x[0] in bad_nodes for x in ival) + if all_missing: + ToStdout("Instance %s cannot be verified as it lives on" + " broken nodes", iname) + else: + ToStdout("Instance %s has missing logical volumes:", iname) + ival.sort() + for node, vol in ival: + if node in bad_nodes: + ToStdout("\tbroken node %s /dev/%s", node, vol) + else: + ToStdout("\t%s /dev/%s", node, vol) + + ToStdout("You need to replace or recreate disks for all the above" + " instances if this message persists after fixing broken nodes.") + retcode = constants.EXIT_FAILURE return retcode @@ -599,7 +648,7 @@ def MasterPing(opts, args): cl = GetClient() cl.QueryClusterInfo() return 0 - except Exception: # pylint: disable-msg=W0703 + except Exception: # pylint: disable=W0703 return 1 @@ -623,9 +672,45 @@ def SearchTags(opts, args): ToStdout("%s %s", path, tag) -def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, - new_confd_hmac_key, new_cds, cds_filename, - force): +def _ReadAndVerifyCert(cert_filename, verify_private_key=False): + """Reads and verifies an X509 certificate. + + @type cert_filename: string + @param cert_filename: the path of the file containing the certificate to + verify encoded in PEM format + @type verify_private_key: bool + @param verify_private_key: whether to verify the private key in addition to + the public certificate + @rtype: string + @return: a string containing the PEM-encoded certificate. + + """ + try: + pem = utils.ReadFile(cert_filename) + except IOError, err: + raise errors.X509CertError(cert_filename, + "Unable to read certificate: %s" % str(err)) + + try: + OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem) + except Exception, err: + raise errors.X509CertError(cert_filename, + "Unable to load certificate: %s" % str(err)) + + if verify_private_key: + try: + OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, pem) + except Exception, err: + raise errors.X509CertError(cert_filename, + "Unable to load private key: %s" % str(err)) + + return pem + + +def _RenewCrypto(new_cluster_cert, new_rapi_cert, #pylint: disable=R0911 + rapi_cert_filename, new_spice_cert, spice_cert_filename, + spice_cacert_filename, new_confd_hmac_key, new_cds, + cds_filename, force): """Renews cluster certificates, keys and secrets. @type new_cluster_cert: bool @@ -634,6 +719,13 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, @param new_rapi_cert: Whether to generate a new RAPI certificate @type rapi_cert_filename: string @param rapi_cert_filename: Path to file containing new RAPI certificate + @type new_spice_cert: bool + @param new_spice_cert: Whether to generate a new SPICE certificate + @type spice_cert_filename: string + @param spice_cert_filename: Path to file containing new SPICE certificate + @type spice_cacert_filename: string + @param spice_cacert_filename: Path to file containing the certificate of the + CA that signed the SPICE certificate @type new_confd_hmac_key: bool @param new_confd_hmac_key: Whether to generate a new HMAC key @type new_cds: bool @@ -645,7 +737,7 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, """ if new_rapi_cert and rapi_cert_filename: - ToStderr("Only one of the --new-rapi-certficate and --rapi-certificate" + ToStderr("Only one of the --new-rapi-certificate and --rapi-certificate" " options can be specified at the same time.") return 1 @@ -655,32 +747,31 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, " the same time.") return 1 - if rapi_cert_filename: - # Read and verify new certificate - try: - rapi_cert_pem = utils.ReadFile(rapi_cert_filename) - - OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, - rapi_cert_pem) - except Exception, err: # pylint: disable-msg=W0703 - ToStderr("Can't load new RAPI certificate from %s: %s" % - (rapi_cert_filename, str(err))) - return 1 + if new_spice_cert and (spice_cert_filename or spice_cacert_filename): + ToStderr("When using --new-spice-certificate, the --spice-certificate" + " and --spice-ca-certificate must not be used.") + return 1 - try: - OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, rapi_cert_pem) - except Exception, err: # pylint: disable-msg=W0703 - ToStderr("Can't load new RAPI private key from %s: %s" % - (rapi_cert_filename, str(err))) - return 1 + if bool(spice_cacert_filename) ^ bool(spice_cert_filename): + ToStderr("Both --spice-certificate and --spice-ca-certificate must be" + " specified.") + return 1 - else: - rapi_cert_pem = None + rapi_cert_pem, spice_cert_pem, spice_cacert_pem = (None, None, None) + try: + if rapi_cert_filename: + rapi_cert_pem = _ReadAndVerifyCert(rapi_cert_filename, True) + if spice_cert_filename: + spice_cert_pem = _ReadAndVerifyCert(spice_cert_filename, True) + spice_cacert_pem = _ReadAndVerifyCert(spice_cacert_filename) + except errors.X509CertError, err: + ToStderr("Unable to load X509 certificate from %s: %s", err[0], err[1]) + return 1 if cds_filename: try: cds = utils.ReadFile(cds_filename) - except Exception, err: # pylint: disable-msg=W0703 + except Exception, err: # pylint: disable=W0703 ToStderr("Can't load new cluster domain secret from %s: %s" % (cds_filename, str(err))) return 1 @@ -695,10 +786,14 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, def _RenewCryptoInner(ctx): ctx.feedback_fn("Updating certificates and keys") - bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, + bootstrap.GenerateClusterCrypto(new_cluster_cert, + new_rapi_cert, + new_spice_cert, new_confd_hmac_key, new_cds, rapi_cert_pem=rapi_cert_pem, + spice_cert_pem=spice_cert_pem, + spice_cacert_pem=spice_cacert_pem, cds=cds) files_to_copy = [] @@ -709,6 +804,10 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename, if new_rapi_cert or rapi_cert_pem: files_to_copy.append(constants.RAPI_CERT_FILE) + if new_spice_cert or spice_cert_pem: + files_to_copy.append(constants.SPICE_CERT_FILE) + files_to_copy.append(constants.SPICE_CACERT_FILE) + if new_confd_hmac_key: files_to_copy.append(constants.CONFD_HMAC_KEY) @@ -737,6 +836,9 @@ def RenewCrypto(opts, args): return _RenewCrypto(opts.new_cluster_cert, opts.new_rapi_cert, opts.rapi_cert, + opts.new_spice_cert, + opts.spice_cert, + opts.spice_cacert, opts.new_confd_hmac_key, opts.new_cluster_domain_secret, opts.cluster_domain_secret, @@ -1234,7 +1336,7 @@ def Epo(opts, args): commands = { - 'init': ( + "init": ( InitCluster, [ArgHost(min=1, max=1)], [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT, HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT, @@ -1244,75 +1346,75 @@ commands = { DEFAULT_IALLOCATOR_OPT, PRIMARY_IP_VERSION_OPT, PREALLOC_WIPE_DISKS_OPT, NODE_PARAMS_OPT, GLOBAL_SHARED_FILEDIR_OPT], "[opts...] ", "Initialises a new cluster configuration"), - 'destroy': ( + "destroy": ( DestroyCluster, ARGS_NONE, [YES_DOIT_OPT], "", "Destroy cluster"), - 'rename': ( + "rename": ( RenameCluster, [ArgHost(min=1, max=1)], [FORCE_OPT, DRY_RUN_OPT], "", "Renames the cluster"), - 'redist-conf': ( + "redist-conf": ( RedistributeConfig, ARGS_NONE, [SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT], "", "Forces a push of the configuration file and ssconf files" " to the nodes in the cluster"), - 'verify': ( + "verify": ( VerifyCluster, ARGS_NONE, [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT, - DRY_RUN_OPT, PRIORITY_OPT], + DRY_RUN_OPT, PRIORITY_OPT, NODEGROUP_OPT], "", "Does a check on the cluster configuration"), - 'verify-disks': ( + "verify-disks": ( VerifyDisks, ARGS_NONE, [PRIORITY_OPT], "", "Does a check on the cluster disk status"), - 'repair-disk-sizes': ( + "repair-disk-sizes": ( RepairDiskSizes, ARGS_MANY_INSTANCES, [DRY_RUN_OPT, PRIORITY_OPT], "", "Updates mismatches in recorded disk sizes"), - 'master-failover': ( + "master-failover": ( MasterFailover, ARGS_NONE, [NOVOTING_OPT], "", "Makes the current node the master"), - 'master-ping': ( + "master-ping": ( MasterPing, ARGS_NONE, [], "", "Checks if the master is alive"), - 'version': ( + "version": ( ShowClusterVersion, ARGS_NONE, [], "", "Shows the cluster version"), - 'getmaster': ( + "getmaster": ( ShowClusterMaster, ARGS_NONE, [], "", "Shows the cluster master"), - 'copyfile': ( + "copyfile": ( ClusterCopyFile, [ArgFile(min=1, max=1)], - [NODE_LIST_OPT, USE_REPL_NET_OPT], + [NODE_LIST_OPT, USE_REPL_NET_OPT, NODEGROUP_OPT], "[-n node...] ", "Copies a file to all (or only some) nodes"), - 'command': ( + "command": ( RunClusterCommand, [ArgCommand(min=1)], - [NODE_LIST_OPT], + [NODE_LIST_OPT, NODEGROUP_OPT], "[-n node...] ", "Runs a command on all (or only some) nodes"), - 'info': ( + "info": ( ShowClusterConfig, ARGS_NONE, [ROMAN_OPT], "[--roman]", "Show cluster configuration"), - 'list-tags': ( + "list-tags": ( ListTags, ARGS_NONE, [], "", "List the tags of the cluster"), - 'add-tags': ( + "add-tags": ( AddTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT], "tag...", "Add tags to the cluster"), - 'remove-tags': ( + "remove-tags": ( RemoveTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT], "tag...", "Remove tags from the cluster"), - 'search-tags': ( + "search-tags": ( SearchTags, [ArgUnknown(min=1, max=1)], [PRIORITY_OPT], "", "Searches the tags on all objects on" " the cluster for a given pattern (regex)"), - 'queue': ( + "queue": ( QueueOps, [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])], [], "drain|undrain|info", "Change queue properties"), - 'watcher': ( + "watcher": ( WatcherOps, [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]), ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])], [], "{pause |continue|info}", "Change watcher properties"), - 'modify': ( + "modify": ( SetClusterParams, ARGS_NONE, [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT, @@ -1325,7 +1427,8 @@ commands = { RenewCrypto, ARGS_NONE, [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT, NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT, - NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT], + NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT, + NEW_SPICE_CERT_OPT, SPICE_CERT_OPT, SPICE_CACERT_OPT], "[opts...]", "Renews cluster certificates, keys and secrets"), "epo": ( @@ -1334,12 +1437,17 @@ commands = { SHUTDOWN_TIMEOUT_OPT, POWER_DELAY_OPT], "[opts...] [args]", "Performs an emergency power-off on given args"), + "activate-master-ip": ( + ActivateMasterIp, ARGS_NONE, [], "", "Activates the master IP"), + "deactivate-master-ip": ( + DeactivateMasterIp, ARGS_NONE, [CONFIRM_OPT], "", + "Deactivates the master IP"), } #: dictionary with aliases for commands aliases = { - 'masterfailover': 'master-failover', + "masterfailover": "master-failover", }