# Copyright 2015 NEC Corporation. All rights reserved. # # 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. import os from magnumclient.common import cliutils as utils 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 DEPRECATING_PARAMS = { "--keypair-id": "--keypair", } def _show_cluster(cluster): del cluster._info['links'] utils.print_dict(cluster._info) @utils.arg('--marker', metavar='', default=None, help=_('The last cluster UUID of the previous page; ' 'displays list of clusters after "marker".')) @utils.arg('--limit', metavar='', type=int, help=_('Maximum number of clusters to return.')) @utils.arg('--sort-key', metavar='', help=_('Column to sort results by.')) @utils.arg('--sort-dir', metavar='', choices=['desc', 'asc'], help=_('Direction to sort. "asc" or "desc".')) @utils.arg('--fields', default=None, metavar='', help=_('Comma-separated list of fields to display. ' 'Available fields: uuid, name, cluster_template_id, ' 'stack_id, status, master_count, node_count, links, ' 'create_timeout' ) ) def do_cluster_list(cs, args): """Print a list of available clusters.""" clusters = cs.clusters.list(marker=args.marker, limit=args.limit, sort_key=args.sort_key, sort_dir=args.sort_dir) columns = [ 'uuid', 'name', 'keypair', 'node_count', 'master_count', 'status' ] columns += utils._get_list_table_columns_and_formatters( args.fields, clusters, exclude_fields=(c.lower() for c in columns))[0] utils.print_list(clusters, columns, {'versions': magnum_utils.print_list_field('versions')}, sortby_index=None) @utils.deprecation_map(DEPRECATING_PARAMS) @utils.arg('--name', metavar='', help=_('Name of the cluster to create.')) @utils.arg('--cluster-template', required=True, metavar='', help=_('ID or name of the cluster template.')) @utils.arg('--keypair-id', dest='keypair', metavar='', default=None, help=utils.deprecation_message( 'UUID or name of the keypair to use for this cluster.', 'keypair')) @utils.arg('--keypair', dest='keypair', metavar='', default=None, help=_('UUID or name of the keypair to use for this cluster.')) @utils.arg('--node-count', metavar='', type=int, default=1, help=_('The cluster node count.')) @utils.arg('--master-count', metavar='', type=int, default=1, help=_('The number of master nodes for the cluster.')) @utils.arg('--discovery-url', metavar='', help=_('Specifies custom discovery url for node discovery.')) @utils.arg('--timeout', metavar='', type=int, default=60, help=_('The timeout for cluster creation in minutes. The default ' 'is 60 minutes.')) def do_cluster_create(cs, args): """Create a cluster.""" cluster_template = cs.cluster_templates.get(args.cluster_template) opts = dict() opts['name'] = args.name opts['cluster_template_id'] = cluster_template.uuid opts['keypair'] = args.keypair opts['node_count'] = args.node_count opts['master_count'] = args.master_count opts['discovery_url'] = args.discovery_url opts['create_timeout'] = args.timeout try: cluster = cs.clusters.create(**opts) # support for non-async in 1.1 if args.magnum_api_version and args.magnum_api_version == '1.1': _show_cluster(cluster) else: uuid = str(cluster._info['uuid']) print("Request to create cluster %s has been accepted." % uuid) except Exception as e: print("Create for cluster %s failed: %s" % (opts['name'], e)) @utils.arg('cluster', metavar='', nargs='+', help=_('ID or name of the (cluster)s to delete.')) def do_cluster_delete(cs, args): """Delete specified cluster.""" for id in args.cluster: try: cs.clusters.delete(id) print("Request to delete cluster %s has been accepted." % id) except Exception as e: print("Delete for cluster %(cluster)s failed: %(e)s" % {'cluster': id, 'e': e}) @utils.arg('cluster', metavar='', help=_('ID or name of the cluster to show.')) @utils.arg('--long', action='store_true', default=False, help=_('Display extra associated cluster template info.')) def do_cluster_show(cs, args): """Show details about the given cluster.""" cluster = cs.clusters.get(args.cluster) if args.long: cluster_template = \ cs.cluster_templates.get(cluster.cluster_template_id) del cluster_template._info['links'], cluster_template._info['uuid'] for key in cluster_template._info: if 'clustertemplate_' + key not in cluster._info: cluster._info['clustertemplate_' + key] = \ cluster_template._info[key] _show_cluster(cluster) @utils.arg('cluster', metavar='', help=_("UUID or name of cluster")) @utils.arg('--rollback', action='store_true', default=False, help=_('Rollback cluster on update failure.')) @utils.arg( 'op', metavar='', choices=['add', 'replace', 'remove'], help=_("Operations: 'add', 'replace' or 'remove'")) @utils.arg( 'attributes', metavar='', nargs='+', action='append', default=[], help=_("Attributes to add/replace or remove " "(only PATH is necessary on remove)")) def do_cluster_update(cs, args): """Update information about the given cluster.""" if args.rollback and args.magnum_api_version and \ args.magnum_api_version in ('1.0', '1.1', '1.2'): raise exceptions.CommandError( "Rollback is not supported in API v%s. " "Please use API v1.3+." % args.magnum_api_version) patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0]) cluster = cs.clusters.update(args.cluster, patch, args.rollback) if args.magnum_api_version and args.magnum_api_version == '1.1': _show_cluster(cluster) else: print("Request to update cluster %s has been accepted." % args.cluster) @utils.arg('cluster', metavar='', help=_('ID or name of the cluster to retrieve config.')) @utils.arg('--dir', metavar='', default='.', help=_('Directory to save the certificate and config files.')) @utils.arg('--force', action='store_true', default=False, help=_('Overwrite files if existing.')) def do_cluster_config(cs, 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. Example: eval $(magnum cluster-config ). """ args.dir = os.path.abspath(args.dir) cluster = cs.clusters.get(args.cluster) if cluster.status not in ('CREATE_COMPLETE', 'UPDATE_COMPLETE', 'ROLLBACK_COMPLETE'): raise exceptions.CommandError("cluster in status %s" % cluster.status) cluster_template = cs.cluster_templates.get(cluster.cluster_template_id) opts = { 'cluster_uuid': cluster.uuid, } if not cluster_template.tls_disabled: tls = _generate_csr_and_key() tls['ca'] = cs.certificates.get(**opts).pem opts['csr'] = tls['csr'] tls['cert'] = cs.certificates.create(**opts).pem for k in ('key', 'cert', 'ca'): fname = "%s/%s.pem" % (args.dir, k) if os.path.exists(fname) and not args.force: raise Exception("File %s exists, aborting." % fname) else: f = open(fname, "w") 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: default/%(name)s\n" "current-context: default/%(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