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
This commit is contained in:
parent
183bf0d48c
commit
6410a15f32
|
@ -226,3 +226,43 @@ class ShellTest(shell_test_base.TestCommandLineArgument):
|
||||||
|
|
||||||
self._test_arg_failure('bay-update test add', _error_msg)
|
self._test_arg_failure('bay-update test add', _error_msg)
|
||||||
self.assertFalse(mock_update.called)
|
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)
|
||||||
|
|
|
@ -14,8 +14,17 @@
|
||||||
|
|
||||||
from magnumclient.common import cliutils as utils
|
from magnumclient.common import cliutils as utils
|
||||||
from magnumclient.common import utils as magnum_utils
|
from magnumclient.common import utils as magnum_utils
|
||||||
|
from magnumclient import exceptions
|
||||||
from magnumclient.i18n import _
|
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):
|
def _show_bay(bay):
|
||||||
del bay._info['links']
|
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])
|
patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0])
|
||||||
bay = cs.bays.update(args.bay, patch)
|
bay = cs.bays.update(args.bay, patch)
|
||||||
_show_bay(bay)
|
_show_bay(bay)
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('bay',
|
||||||
|
metavar='<bay>',
|
||||||
|
help='ID or name of the bay to retrieve config.')
|
||||||
|
@utils.arg('--dir',
|
||||||
|
metavar='<dir>',
|
||||||
|
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-name>).
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
|
@ -13,3 +13,5 @@ oslo.serialization>=1.10.0 # Apache-2.0
|
||||||
oslo.utils>=3.14.0 # Apache-2.0
|
oslo.utils>=3.14.0 # Apache-2.0
|
||||||
os-client-config>=1.13.1 # Apache-2.0
|
os-client-config>=1.13.1 # Apache-2.0
|
||||||
PrettyTable<0.8,>=0.7 # BSD
|
PrettyTable<0.8,>=0.7 # BSD
|
||||||
|
cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue