From 6410a15f326d90f47e9956501ecabcc4f4b559f6 Mon Sep 17 00:00:00 2001 From: Ricardo Rocha Date: Thu, 14 Jul 2016 14:38:05 +0200 Subject: [PATCH] Add shell command bay-config Generate the required configuration for the corresponding native client of a given bay. Output the appropriate export or setenv commands with the native client's environment variables, ready for clients to source. In the case of kubernetes also generate a config file which is pointed by the KUBECONFIG env var. In the case of docker the config file does not support this and all goes into env vars. Change-Id: I44700b97ba3efc4c818112c95f80adf723048c5a Implements: blueprint magnum-coe-client-config --- magnumclient/tests/v1/test_bays_shell.py | 40 ++++++ magnumclient/v1/bays_shell.py | 158 +++++++++++++++++++++++ requirements.txt | 2 + 3 files changed, 200 insertions(+) diff --git a/magnumclient/tests/v1/test_bays_shell.py b/magnumclient/tests/v1/test_bays_shell.py index ad1c5114..1a59e680 100644 --- a/magnumclient/tests/v1/test_bays_shell.py +++ b/magnumclient/tests/v1/test_bays_shell.py @@ -226,3 +226,43 @@ class ShellTest(shell_test_base.TestCommandLineArgument): self._test_arg_failure('bay-update test add', _error_msg) self.assertFalse(mock_update.called) + + @mock.patch('magnumclient.v1.baymodels.BayModelManager.get') + @mock.patch('magnumclient.v1.bays.BayManager.get') + def test_bay_config_success(self, mock_bay, mock_baymodel): + mock_bay.return_value = FakeBay(status='UPDATE_COMPLETE') + self._test_arg_success('bay-config xxx') + self.assertTrue(mock_bay.called) + + mock_bay.return_value = FakeBay(status='CREATE_COMPLETE') + self._test_arg_success('bay-config xxx') + self.assertTrue(mock_bay.called) + + self._test_arg_success('bay-config --dir /tmp xxx') + self.assertTrue(mock_bay.called) + + self._test_arg_success('bay-config --force xxx') + self.assertTrue(mock_bay.called) + + self._test_arg_success('bay-config --dir /tmp --force xxx') + self.assertTrue(mock_bay.called) + + @mock.patch('magnumclient.v1.baymodels.BayModelManager.get') + @mock.patch('magnumclient.v1.bays.BayManager.get') + def test_bay_config_failure_wrong_status(self, mock_bay, mock_baymodel): + mock_bay.return_value = FakeBay(status='CREATE_IN_PROGRESS') + self.assertRaises(exceptions.CommandError, + self._test_arg_failure, + 'bay-config xxx', + ['.*?^Bay in status: ']) + + @mock.patch('magnumclient.v1.bays.BayManager.get') + def test_bay_config_failure_no_arg(self, mock_bay): + self._test_arg_failure('bay-config', self._few_argument_error) + self.assertFalse(mock_bay.called) + + @mock.patch('magnumclient.v1.bays.BayManager.get') + def test_bay_config_failure_wrong_arg(self, mock_bay): + self._test_arg_failure('bay-config xxx yyy', + self._unrecognized_arg_error) + self.assertFalse(mock_bay.called) diff --git a/magnumclient/v1/bays_shell.py b/magnumclient/v1/bays_shell.py index 32b3b4a2..9847aff8 100644 --- a/magnumclient/v1/bays_shell.py +++ b/magnumclient/v1/bays_shell.py @@ -14,8 +14,17 @@ 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 +import os + def _show_bay(bay): del bay._info['links'] @@ -149,3 +158,152 @@ def do_bay_update(cs, args): patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0]) bay = cs.bays.update(args.bay, patch) _show_bay(bay) + + +@utils.arg('bay', + metavar='', + help='ID or name of the bay 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_bay_config(cs, args): + """Configure native client to access bay. + + You can source the output of this command to get the native client of the + corresponding COE configured to access the bay. + + Example: eval $(magnum bay-config ). + """ + bay = cs.bays.get(args.bay) + if bay.status not in ('CREATE_COMPLETE', 'UPDATE_COMPLETE'): + raise exceptions.CommandError("Bay in status %s" % bay.status) + baymodel = cs.baymodels.get(bay.baymodel_id) + opts = { + 'bay_uuid': bay.uuid, + } + + if not baymodel.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_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.""" + 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': not baymodel.tls_disabled} + ) + 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': not baymodel.tls_disabled} + ) + + 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), + 'key': key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption()), + } + + return result diff --git a/requirements.txt b/requirements.txt index 507eda80..dca1bb4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,5 @@ oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.14.0 # Apache-2.0 os-client-config>=1.13.1 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD +cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0 +