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
This commit is contained in:
Spyros Trigazis 2017-07-26 10:18:43 +00:00
parent a9918d16b8
commit 5f3b69b15c
5 changed files with 196 additions and 236 deletions

View File

@ -16,6 +16,14 @@
# under the License. # under the License.
import json 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 import exceptions as exc
from magnumclient.i18n import _ from magnumclient.i18n import _
@ -142,3 +150,116 @@ def handle_json_from_file(json_arg):
raise exc.InvalidAttribute(err) raise exc.InvalidAttribute(err)
return json_arg 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

View File

@ -12,7 +12,10 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
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 osc_lib.command import command from osc_lib.command import command
@ -242,3 +245,68 @@ class UpdateCluster(command.Command):
patch) patch)
print("Request to update cluster %s has been accepted." % print("Request to update cluster %s has been accepted." %
parsed_args.cluster) 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='<cluster>',
help=_('The name or UUID of cluster to update'))
parser.add_argument(
'--dir',
metavar='<dir>',
default='.',
help=_('Directory to save the certificate and config files.'))
parser.add_argument(
'--force',
metavar='<force>',
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))

View File

@ -17,12 +17,6 @@ from magnumclient.common import utils as magnum_utils
from magnumclient import exceptions 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 import os
@ -242,7 +236,7 @@ def do_bay_config(cs, args):
} }
if not baymodel.tls_disabled: if not baymodel.tls_disabled:
tls = _generate_csr_and_key() tls = magnum_utils.generate_csr_and_key()
tls['ca'] = cs.certificates.get(**opts).pem tls['ca'] = cs.certificates.get(**opts).pem
opts['csr'] = tls['csr'] opts['csr'] = tls['csr']
tls['cert'] = cs.certificates.create(**opts).pem tls['cert'] = cs.certificates.create(**opts).pem
@ -255,112 +249,5 @@ def do_bay_config(cs, args):
f.write(tls[k]) f.write(tls[k])
f.close() f.close()
print(_config_bay(bay, baymodel, cfg_dir=args.dir, force=args.force)) print(magnum_utils.config_cluster(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

View File

@ -19,13 +19,6 @@ from magnumclient.common import utils as magnum_utils
from magnumclient import exceptions 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
# Maps old parameter names to their new names and whether they are required # Maps old parameter names to their new names and whether they are required
# e.g. keypair-id to keypair # e.g. keypair-id to keypair
@ -266,7 +259,7 @@ def do_cluster_config(cs, args):
} }
if not cluster_template.tls_disabled: 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 tls['ca'] = cs.certificates.get(**opts).pem
opts['csr'] = tls['csr'] opts['csr'] = tls['csr']
tls['cert'] = cs.certificates.create(**opts).pem tls['cert'] = cs.certificates.create(**opts).pem
@ -279,115 +272,5 @@ def do_cluster_config(cs, args):
f.write(tls[k]) f.write(tls[k])
f.close() f.close()
print(_config_cluster(cluster, cluster_template, print(magnum_utils.config_cluster(cluster, cluster_template,
cfg_dir=args.dir, force=args.force)) 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

View File

@ -41,6 +41,7 @@ openstack.container_infra.v1 =
coe_cluster_delete = magnumclient.osc.v1.clusters:DeleteCluster coe_cluster_delete = magnumclient.osc.v1.clusters:DeleteCluster
coe_cluster_show = magnumclient.osc.v1.clusters:ShowCluster coe_cluster_show = magnumclient.osc.v1.clusters:ShowCluster
coe_cluster_update = magnumclient.osc.v1.clusters:UpdateCluster coe_cluster_update = magnumclient.osc.v1.clusters:UpdateCluster
coe_cluster_config = magnumclient.osc.v1.clusters:ConfigCluster
[build_sphinx] [build_sphinx]
source-dir = doc/source source-dir = doc/source