From 5f3b69b15c3778a1a8c80e169f688a131d560ea0 Mon Sep 17 00:00:00 2001 From: Spyros Trigazis Date: Wed, 26 Jul 2017 10:18:43 +0000 Subject: [PATCH] OSC: Add cluster config command Move certificate generation and config generation in magnum utils so that all clusters_shell, bays_shell and OSC can use. * Remove / from context name (see #1705480) * Use absolute paths for the certificates in kubeconfig It's the same principle like #1614682 Change-Id: I5b8bb11b199b7646a984c7171f3853d3e73923ec Implements: blueprint openstackclient-support Related-Bug: #1705480 Related-Bug: #1614682 --- magnumclient/common/utils.py | 121 +++++++++++++++++++++++++++++ magnumclient/osc/v1/clusters.py | 68 +++++++++++++++++ magnumclient/v1/bays_shell.py | 119 +---------------------------- magnumclient/v1/clusters_shell.py | 123 +----------------------------- setup.cfg | 1 + 5 files changed, 196 insertions(+), 236 deletions(-) diff --git a/magnumclient/common/utils.py b/magnumclient/common/utils.py index 9c26ac7f..e13d753e 100644 --- a/magnumclient/common/utils.py +++ b/magnumclient/common/utils.py @@ -16,6 +16,14 @@ # under the License. import json +import os + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography import x509 +from cryptography.x509.oid import NameOID from magnumclient import exceptions as exc from magnumclient.i18n import _ @@ -142,3 +150,116 @@ def handle_json_from_file(json_arg): raise exc.InvalidAttribute(err) return json_arg + + +def config_cluster(cluster, cluster_template, cfg_dir, force=False): + """Return and write configuration for the given cluster.""" + if cluster_template.coe == 'kubernetes': + return _config_cluster_kubernetes(cluster, cluster_template, + cfg_dir, force) + elif (cluster_template.coe == 'swarm' + or cluster_template.coe == 'swarm-mode'): + return _config_cluster_swarm(cluster, cluster_template, cfg_dir, force) + + +def _config_cluster_kubernetes(cluster, cluster_template, + cfg_dir, force=False): + """Return and write configuration for the given kubernetes cluster.""" + cfg_file = "%s/config" % cfg_dir + if cluster_template.tls_disabled: + cfg = ("apiVersion: v1\n" + "clusters:\n" + "- cluster:\n" + " server: %(api_address)s\n" + " name: %(name)s\n" + "contexts:\n" + "- context:\n" + " cluster: %(name)s\n" + " user: %(name)s\n" + " name: %(name)s\n" + "current-context: %(name)s\n" + "kind: Config\n" + "preferences: {}\n" + "users:\n" + "- name: %(name)s'\n" + % {'name': cluster.name, 'api_address': cluster.api_address}) + else: + cfg = ("apiVersion: v1\n" + "clusters:\n" + "- cluster:\n" + " certificate-authority: %(cfg_dir)s/ca.pem\n" + " server: %(api_address)s\n" + " name: %(name)s\n" + "contexts:\n" + "- context:\n" + " cluster: %(name)s\n" + " user: %(name)s\n" + " name: %(name)s\n" + "current-context: %(name)s\n" + "kind: Config\n" + "preferences: {}\n" + "users:\n" + "- name: %(name)s\n" + " user:\n" + " client-certificate: %(cfg_dir)s/cert.pem\n" + " client-key: %(cfg_dir)s/key.pem\n" + % {'name': cluster.name, + 'api_address': cluster.api_address, + 'cfg_dir': cfg_dir}) + + if os.path.exists(cfg_file) and not force: + raise exc.CommandError("File %s exists, aborting." % cfg_file) + else: + f = open(cfg_file, "w") + f.write(cfg) + f.close() + if 'csh' in os.environ['SHELL']: + return "setenv KUBECONFIG %s\n" % cfg_file + else: + return "export KUBECONFIG=%s\n" % cfg_file + + +def _config_cluster_swarm(cluster, cluster_template, cfg_dir, force=False): + """Return and write configuration for the given swarm cluster.""" + tls = "" if cluster_template.tls_disabled else True + if 'csh' in os.environ['SHELL']: + result = ("setenv DOCKER_HOST %(docker_host)s\n" + "setenv DOCKER_CERT_PATH %(cfg_dir)s\n" + "setenv DOCKER_TLS_VERIFY %(tls)s\n" + % {'docker_host': cluster.api_address, + 'cfg_dir': cfg_dir, + 'tls': tls} + ) + else: + result = ("export DOCKER_HOST=%(docker_host)s\n" + "export DOCKER_CERT_PATH=%(cfg_dir)s\n" + "export DOCKER_TLS_VERIFY=%(tls)s\n" + % {'docker_host': cluster.api_address, + 'cfg_dir': cfg_dir, + 'tls': tls} + ) + + return result + + +def generate_csr_and_key(): + """Return a dict with a new csr and key.""" + key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend()) + + csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"Magnum User"), + ])).sign(key, hashes.SHA256(), default_backend()) + + result = { + 'csr': csr.public_bytes( + encoding=serialization.Encoding.PEM).decode("utf-8"), + 'key': key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption()).decode("utf-8"), + } + + return result diff --git a/magnumclient/osc/v1/clusters.py b/magnumclient/osc/v1/clusters.py index d658ec94..274ed932 100644 --- a/magnumclient/osc/v1/clusters.py +++ b/magnumclient/osc/v1/clusters.py @@ -12,7 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import os + from magnumclient.common import utils as magnum_utils +from magnumclient import exceptions from magnumclient.i18n import _ from osc_lib.command import command @@ -242,3 +245,68 @@ class UpdateCluster(command.Command): patch) print("Request to update cluster %s has been accepted." % parsed_args.cluster) + + +class ConfigCluster(command.Command): + _description = _("Get Configuration for a Cluster") + + def get_parser(self, prog_name): + parser = super(ConfigCluster, self).get_parser(prog_name) + parser.add_argument( + 'cluster', + metavar='', + help=_('The name or UUID of cluster to update')) + parser.add_argument( + '--dir', + metavar='', + default='.', + help=_('Directory to save the certificate and config files.')) + parser.add_argument( + '--force', + metavar='', + default=False, + help=_('Directory to save the certificate and config files.')) + + return parser + + def take_action(self, parsed_args): + """Configure native client to access cluster. + + You can source the output of this command to get the native client of + the corresponding COE configured to access the cluster. + + """ + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + + parsed_args.dir = os.path.abspath(parsed_args.dir) + cluster = mag_client.clusters.get(parsed_args.cluster) + if cluster.status not in ('CREATE_COMPLETE', 'UPDATE_COMPLETE', + 'ROLLBACK_COMPLETE'): + raise exceptions.CommandError("cluster in status %s" % + cluster.status) + cluster_template = mag_client.cluster_templates.get( + cluster.cluster_template_id) + opts = { + 'cluster_uuid': cluster.uuid, + } + + if not cluster_template.tls_disabled: + tls = magnum_utils.generate_csr_and_key() + tls['ca'] = mag_client.certificates.get(**opts).pem + opts['csr'] = tls['csr'] + tls['cert'] = mag_client.certificates.create(**opts).pem + for k in ('key', 'cert', 'ca'): + fname = "%s/%s.pem" % (parsed_args.dir, k) + if os.path.exists(fname) and not parsed_args.force: + raise Exception("File %s exists, aborting." % fname) + else: + f = open(fname, "w") + f.write(tls[k]) + f.close() + + print(magnum_utils.config_cluster(cluster, + cluster_template, + parsed_args.dir, + force=parsed_args.force)) diff --git a/magnumclient/v1/bays_shell.py b/magnumclient/v1/bays_shell.py index c6d274e5..689e84ff 100644 --- a/magnumclient/v1/bays_shell.py +++ b/magnumclient/v1/bays_shell.py @@ -17,12 +17,6 @@ from magnumclient.common import utils as magnum_utils from magnumclient import exceptions from magnumclient.i18n import _ -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives import serialization -from cryptography import x509 -from cryptography.x509.oid import NameOID import os @@ -242,7 +236,7 @@ def do_bay_config(cs, args): } if not baymodel.tls_disabled: - tls = _generate_csr_and_key() + tls = magnum_utils.generate_csr_and_key() tls['ca'] = cs.certificates.get(**opts).pem opts['csr'] = tls['csr'] tls['cert'] = cs.certificates.create(**opts).pem @@ -255,112 +249,5 @@ def do_bay_config(cs, args): f.write(tls[k]) f.close() - print(_config_bay(bay, baymodel, cfg_dir=args.dir, force=args.force)) - - -def _config_bay(bay, baymodel, cfg_dir, force=False): - """Return and write configuration for the given bay.""" - if baymodel.coe == 'kubernetes': - return _config_bay_kubernetes(bay, baymodel, cfg_dir, force) - elif baymodel.coe == 'swarm': - return _config_bay_swarm(bay, baymodel, cfg_dir, force) - - -def _config_bay_kubernetes(bay, baymodel, cfg_dir, force=False): - """Return and write configuration for the given kubernetes bay.""" - cfg_file = "%s/config" % cfg_dir - if baymodel.tls_disabled: - cfg = ("apiVersion: v1\n" - "clusters:\n" - "- cluster:\n" - " server: %(api_address)s\n" - " name: %(name)s\n" - "contexts:\n" - "- context:\n" - " cluster: %(name)s\n" - " user: %(name)s\n" - " name: default/%(name)s\n" - "current-context: default/%(name)s\n" - "kind: Config\n" - "preferences: {}\n" - "users:\n" - "- name: %(name)s'\n" - % {'name': bay.name, 'api_address': bay.api_address}) - else: - cfg = ("apiVersion: v1\n" - "clusters:\n" - "- cluster:\n" - " certificate-authority: ca.pem\n" - " server: %(api_address)s\n" - " name: %(name)s\n" - "contexts:\n" - "- context:\n" - " cluster: %(name)s\n" - " user: %(name)s\n" - " name: default/%(name)s\n" - "current-context: default/%(name)s\n" - "kind: Config\n" - "preferences: {}\n" - "users:\n" - "- name: %(name)s\n" - " user:\n" - " client-certificate: cert.pem\n" - " client-key: key.pem\n" - % {'name': bay.name, 'api_address': bay.api_address}) - - if os.path.exists(cfg_file) and not force: - raise exceptions.CommandError("File %s exists, aborting." % cfg_file) - else: - f = open(cfg_file, "w") - f.write(cfg) - f.close() - if 'csh' in os.environ['SHELL']: - return "setenv KUBECONFIG %s\n" % cfg_file - else: - return "export KUBECONFIG=%s\n" % cfg_file - - -def _config_bay_swarm(bay, baymodel, cfg_dir, force=False): - """Return and write configuration for the given swarm bay.""" - tls = "" if baymodel.tls_disabled else True - if 'csh' in os.environ['SHELL']: - result = ("setenv DOCKER_HOST %(docker_host)s\n" - "setenv DOCKER_CERT_PATH %(cfg_dir)s\n" - "setenv DOCKER_TLS_VERIFY %(tls)s\n" - % {'docker_host': bay.api_address, - 'cfg_dir': cfg_dir, - 'tls': tls} - ) - else: - result = ("export DOCKER_HOST=%(docker_host)s\n" - "export DOCKER_CERT_PATH=%(cfg_dir)s\n" - "export DOCKER_TLS_VERIFY=%(tls)s\n" - % {'docker_host': bay.api_address, - 'cfg_dir': cfg_dir, - 'tls': tls} - ) - - return result - - -def _generate_csr_and_key(): - """Return a dict with a new csr and key.""" - key = rsa.generate_private_key( - public_exponent=65537, - key_size=2048, - backend=default_backend()) - - csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, u"Magnum User"), - ])).sign(key, hashes.SHA256(), default_backend()) - - result = { - 'csr': csr.public_bytes( - encoding=serialization.Encoding.PEM).decode("utf-8"), - 'key': key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption()).decode("utf-8"), - } - - return result + print(magnum_utils.config_cluster(bay, baymodel, cfg_dir=args.dir, + force=args.force)) diff --git a/magnumclient/v1/clusters_shell.py b/magnumclient/v1/clusters_shell.py index f3456d5a..4fa77be0 100644 --- a/magnumclient/v1/clusters_shell.py +++ b/magnumclient/v1/clusters_shell.py @@ -19,13 +19,6 @@ from magnumclient.common import utils as magnum_utils from magnumclient import exceptions from magnumclient.i18n import _ -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives import serialization -from cryptography import x509 -from cryptography.x509.oid import NameOID - # Maps old parameter names to their new names and whether they are required # e.g. keypair-id to keypair @@ -266,7 +259,7 @@ def do_cluster_config(cs, args): } if not cluster_template.tls_disabled: - tls = _generate_csr_and_key() + tls = magnum_utils.generate_csr_and_key() tls['ca'] = cs.certificates.get(**opts).pem opts['csr'] = tls['csr'] tls['cert'] = cs.certificates.create(**opts).pem @@ -279,115 +272,5 @@ def do_cluster_config(cs, args): f.write(tls[k]) f.close() - print(_config_cluster(cluster, cluster_template, - cfg_dir=args.dir, force=args.force)) - - -def _config_cluster(cluster, cluster_template, cfg_dir, force=False): - """Return and write configuration for the given cluster.""" - if cluster_template.coe == 'kubernetes': - return _config_cluster_kubernetes(cluster, cluster_template, - cfg_dir, force) - elif cluster_template.coe == 'swarm': - return _config_cluster_swarm(cluster, cluster_template, cfg_dir, force) - - -def _config_cluster_kubernetes(cluster, cluster_template, - cfg_dir, force=False): - """Return and write configuration for the given kubernetes cluster.""" - cfg_file = "%s/config" % cfg_dir - if cluster_template.tls_disabled: - cfg = ("apiVersion: v1\n" - "clusters:\n" - "- cluster:\n" - " server: %(api_address)s\n" - " name: %(name)s\n" - "contexts:\n" - "- context:\n" - " cluster: %(name)s\n" - " user: %(name)s\n" - " name: %(name)s\n" - "current-context: %(name)s\n" - "kind: Config\n" - "preferences: {}\n" - "users:\n" - "- name: %(name)s'\n" - % {'name': cluster.name, 'api_address': cluster.api_address}) - else: - cfg = ("apiVersion: v1\n" - "clusters:\n" - "- cluster:\n" - " certificate-authority: ca.pem\n" - " server: %(api_address)s\n" - " name: %(name)s\n" - "contexts:\n" - "- context:\n" - " cluster: %(name)s\n" - " user: %(name)s\n" - " name: default/%(name)s\n" - "current-context: default/%(name)s\n" - "kind: Config\n" - "preferences: {}\n" - "users:\n" - "- name: %(name)s\n" - " user:\n" - " client-certificate: cert.pem\n" - " client-key: key.pem\n" - % {'name': cluster.name, 'api_address': cluster.api_address}) - - if os.path.exists(cfg_file) and not force: - raise exceptions.CommandError("File %s exists, aborting." % cfg_file) - else: - f = open(cfg_file, "w") - f.write(cfg) - f.close() - if 'csh' in os.environ['SHELL']: - return "setenv KUBECONFIG %s\n" % cfg_file - else: - return "export KUBECONFIG=%s\n" % cfg_file - - -def _config_cluster_swarm(cluster, cluster_template, cfg_dir, force=False): - """Return and write configuration for the given swarm cluster.""" - tls = "" if cluster_template.tls_disabled else True - if 'csh' in os.environ['SHELL']: - result = ("setenv DOCKER_HOST %(docker_host)s\n" - "setenv DOCKER_CERT_PATH %(cfg_dir)s\n" - "setenv DOCKER_TLS_VERIFY %(tls)s\n" - % {'docker_host': cluster.api_address, - 'cfg_dir': cfg_dir, - 'tls': tls} - ) - else: - result = ("export DOCKER_HOST=%(docker_host)s\n" - "export DOCKER_CERT_PATH=%(cfg_dir)s\n" - "export DOCKER_TLS_VERIFY=%(tls)s\n" - % {'docker_host': cluster.api_address, - 'cfg_dir': cfg_dir, - 'tls': tls} - ) - - return result - - -def _generate_csr_and_key(): - """Return a dict with a new csr and key.""" - key = rsa.generate_private_key( - public_exponent=65537, - key_size=2048, - backend=default_backend()) - - csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, u"Magnum User"), - ])).sign(key, hashes.SHA256(), default_backend()) - - result = { - 'csr': csr.public_bytes( - encoding=serialization.Encoding.PEM).decode("utf-8"), - 'key': key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption()).decode("utf-8"), - } - - return result + print(magnum_utils.config_cluster(cluster, cluster_template, + cfg_dir=args.dir, force=args.force)) diff --git a/setup.cfg b/setup.cfg index 5c5fa8b9..db2d07f1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ openstack.container_infra.v1 = coe_cluster_delete = magnumclient.osc.v1.clusters:DeleteCluster coe_cluster_show = magnumclient.osc.v1.clusters:ShowCluster coe_cluster_update = magnumclient.osc.v1.clusters:UpdateCluster + coe_cluster_config = magnumclient.osc.v1.clusters:ConfigCluster [build_sphinx] source-dir = doc/source