# -*- coding: utf-8 -*- # # Copyright 2012 OpenStack LLC. # 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 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 oslo_serialization import base64 from oslo_serialization import jsonutils from magnumclient import exceptions as exc from magnumclient.i18n import _ def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None): """Generate common filters for any list request. :param marker: entity ID from which to start returning entities. :param limit: maximum number of entities to return. :param sort_key: field to use for sorting. :param sort_dir: direction of sorting: 'asc' or 'desc'. :returns: list of string filters. """ filters = [] if isinstance(limit, int): filters.append('limit=%s' % limit) if marker is not None: filters.append('marker=%s' % marker) if sort_key is not None: filters.append('sort_key=%s' % sort_key) if sort_dir is not None: filters.append('sort_dir=%s' % sort_dir) return filters def split_and_deserialize(string): """Split and try to JSON deserialize a string. Gets a string with the KEY=VALUE format, split it (using '=' as the separator) and try to JSON deserialize the VALUE. :returns: A tuple of (key, value). """ try: key, value = string.split("=", 1) except ValueError: raise exc.CommandError(_('Attributes must be a list of ' 'PATH=VALUE not "%s"') % string) try: value = jsonutils.loads(value) except ValueError: pass return (key, value) def args_array_to_patch(op, attributes): patch = [] for attr in attributes: # Sanitize if not attr.startswith('/'): attr = '/' + attr if op in ['add', 'replace']: path, value = split_and_deserialize(attr) if path == "/labels" or path == "/health_status_reason": a = [] a.append(value) value = str(handle_labels(a)) patch.append({'op': op, 'path': path, 'value': value}) else: patch.append({'op': op, 'path': path, 'value': value}) elif op == "remove": # For remove only the key is needed patch.append({'op': op, 'path': attr}) else: raise exc.CommandError(_('Unknown PATCH operation: %s') % op) return patch def handle_labels(labels): labels = format_labels(labels) if 'mesos_slave_executor_env_file' in labels: environment_variables_data = handle_json_from_file( labels['mesos_slave_executor_env_file']) labels['mesos_slave_executor_env_variables'] = jsonutils.dumps( environment_variables_data) return labels def format_labels(lbls, parse_comma=True): '''Reformat labels into dict of format expected by the API.''' if not lbls: return {} if parse_comma: # expect multiple invocations of --labels but fall back # to either , or ; delimited if only one --labels is specified if len(lbls) == 1 and lbls[0].count('=') > 1: lbls = lbls[0].replace(';', ',').split(',') labels = {} for lbl in lbls: try: (k, v) = lbl.split(('='), 1) except ValueError: raise exc.CommandError(_('labels must be a list of KEY=VALUE ' 'not %s') % lbl) if k not in labels: labels[k] = v else: labels[k] += ",%s" % v return labels def print_list_field(field): return lambda obj: ', '.join(getattr(obj, field)) def handle_json_from_file(json_arg): """Attempts to read JSON file by the file url. :param json_arg: May be a file name containing the JSON. :returns: A list or dictionary parsed from JSON. """ try: with open(json_arg, 'r') as f: json_arg = f.read().strip() json_arg = jsonutils.loads(json_arg) except IOError as e: err = _("Cannot get JSON from file '%(file)s'. " "Error: %(err)s") % {'err': e, 'file': json_arg} raise exc.InvalidAttribute(err) except ValueError as e: err = (_("For JSON: '%(string)s', error: '%(err)s'") % {'err': e, 'string': json_arg}) raise exc.InvalidAttribute(err) return json_arg def config_cluster(cluster, cluster_template, cfg_dir, force=False, certs=None, use_keystone=False): """Return and write configuration for the given cluster.""" if cluster_template.coe == 'kubernetes': return _config_cluster_kubernetes(cluster, cluster_template, cfg_dir, force, certs, use_keystone) elif (cluster_template.coe == 'swarm' or cluster_template.coe == 'swarm-mode'): return _config_cluster_swarm(cluster, cluster_template, cfg_dir, force, certs) def _config_cluster_kubernetes(cluster, cluster_template, cfg_dir, force=False, certs=None, use_keystone=False): """Return and write configuration for the given kubernetes cluster.""" cfg_file = "%s/config" % cfg_dir if cluster_template.tls_disabled or certs is None: 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: if not use_keystone: cfg = ("apiVersion: v1\n" "clusters:\n" "- cluster:\n" " certificate-authority-data: %(ca)s\n" " server: %(api_address)s\n" " name: %(name)s\n" "contexts:\n" "- context:\n" " cluster: %(name)s\n" " user: admin\n" " name: default\n" "current-context: default\n" "kind: Config\n" "preferences: {}\n" "users:\n" "- name: admin\n" " user:\n" " client-certificate-data: %(cert)s\n" " client-key-data: %(key)s\n" % {'name': cluster.name, 'api_address': cluster.api_address, 'key': base64.encode_as_text(certs['key']), 'cert': base64.encode_as_text(certs['cert']), 'ca': base64.encode_as_text(certs['ca'])}) else: cfg = ("apiVersion: v1\n" "clusters:\n" "- cluster:\n" " certificate-authority-data: %(ca)s\n" " server: %(api_address)s\n" " name: %(name)s\n" "contexts:\n" "- context:\n" " cluster: %(name)s\n" " user: openstackuser\n" " name: openstackuser@kubernetes\n" "current-context: openstackuser@kubernetes\n" "kind: Config\n" "preferences: {}\n" "users:\n" "- name: openstackuser\n" " user:\n" " exec:\n" " command: /bin/bash\n" " apiVersion: client.authentication.k8s.io/v1beta1\n" " args:\n" " - -c\n" " - >\n" " if [ -z ${OS_TOKEN} ]; then\n" " echo 'Error: Missing OpenStack credential from environment variable $OS_TOKEN' > /dev/stderr\n" # noqa " exit 1\n" " else\n" " echo '{ \"apiVersion\": \"client.authentication.k8s.io/v1beta1\", \"kind\": \"ExecCredential\", \"status\": { \"token\": \"'\"${OS_TOKEN}\"'\"}}'\n" # noqa " fi\n" % {'name': cluster.name, 'api_address': cluster.api_address, 'ca': base64.encode_as_text(certs['ca'])}) 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, certs=None): """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"admin"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"system:masters") ])).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