diff --git a/doc/source/tls.rst b/doc/source/tls.rst new file mode 100644 index 0000000000..446daaf2f5 --- /dev/null +++ b/doc/source/tls.rst @@ -0,0 +1,106 @@ +.. + Copyright 2015 Rackspace + 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. + +======================== +Transport Layer Security +======================== + +Magnum uses TLS to secure communication between a Bay's services and the +outside world. This includes not only Magnum itself, but also the end-user +when they choose to use native client libraries to interact with the Bay. +Magnum also uses TLS certificates for client authentication, which means each +client needs a valid certificate to communicate with a Bay. + +TLS is a complex subject, and many guides on it exist already. This guide will +not attempt to fully describe TLS, only the necessary pieces to get a client +set up to talk to a Bay with TLS. A more indepth guide on TLS can be found in +the `OpenSSL Cookbook `_ +by Ivan Ristić. + + +Generating a Client Key and Certificate Signing Request +======================================================= + +The first step to setting up a client is to generate your personal private key. +This is essentially a cryptographically generated string of bytes. It should be +protected as a password. To generate an RSA key, you will use the 'genrsa' +command of the 'openssl' tool. + +:: + + openssl genrsa -out client.key 4096 + +This command generates a 4096 byte RSA key at client.key. + +Next, you will need to generate a certificate signing request (CSR). This will +be used by Magnum to generate a signed certificate you will use to communicate +with the Bay. It is used by the Bay to secure the connection and validate you +are you who say you are. + +To generate a CSR for client authentication, openssl requires a config file +that specifies a few values. Below is a simple template, just fill in the 'CN' +value with your name and save it as client.conf + +:: + + [req] + distinguished_name = req_distinguished_name + req_extensions = req_ext + x509_extensions = req_ext + prompt = no + [req_distinguished_name] + CN = Your Name + [req_ext] + extendedKeyUsage = clientAuth + +Once you have client.conf, you can run the openssl 'req' command to generate +the CSR. + +:: + + openssl req -new -days 365 + -config client.conf + -reqexts req_ext + -extensions req_ext + -key client.key + -out client.csr + + +Now that you have your client CSR, you can use the Magnum CLI to send it off +to Magnum to get it signed. + +:: + + magnum ca-sign --bay --csr client.csr > client.crt + +The final piece you need to retrieve is the CA certificate for the bay. This +is used by your native client to ensure you're only communicating with hosts +that Magnum set up. + +:: + + magnum ca-show --bay > ca.crt + +Once you have all of these pieces, you can configure your native client. Below +is an example for Docker. + +:: + + docker -H tcp://:2376 --tls --tlsverify \ + --tlscacert ca.crt \ + --tlskey client.key \ + --tlscert client.crt + info diff --git a/magnum/api/controllers/v1/baymodel.py b/magnum/api/controllers/v1/baymodel.py index 01b3f50881..53ed22dd31 100644 --- a/magnum/api/controllers/v1/baymodel.py +++ b/magnum/api/controllers/v1/baymodel.py @@ -122,6 +122,9 @@ class BayModel(base.APIBase): labels = wtypes.DictType(str, str) """One or more key/value pairs""" + insecure = wsme.wsattr(types.boolean, default=False) + """Indicates whether the TLS should be disabled""" + def __init__(self, **kwargs): self.fields = [] for field in objects.BayModel.fields: diff --git a/magnum/conductor/handlers/bay_conductor.py b/magnum/conductor/handlers/bay_conductor.py index 01578b5b06..121f95b144 100644 --- a/magnum/conductor/handlers/bay_conductor.py +++ b/magnum/conductor/handlers/bay_conductor.py @@ -11,6 +11,7 @@ # 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 uuid from heatclient.common import template_utils from heatclient import exc @@ -124,6 +125,7 @@ class Handler(object): try: # Generate certificate and set the cert reference to bay cert_manager.generate_certificates_to_bay(bay) + bay.uuid = uuid.uuid4() created_stack = _create_stack(context, osc, bay, bay_create_timeout) except exc.HTTPBadRequest as e: diff --git a/magnum/conductor/template_definition.py b/magnum/conductor/template_definition.py index 3bb71de65b..2ee0cb0dc6 100644 --- a/magnum/conductor/template_definition.py +++ b/magnum/conductor/template_definition.py @@ -20,6 +20,7 @@ from pkg_resources import iter_entry_points import requests import six +from magnum.common import clients from magnum.common import exception from magnum.common import paths from magnum.i18n import _ @@ -107,16 +108,16 @@ class ParameterMapping(object): value = None if (self.baymodel_attr and - getattr(baymodel, self.baymodel_attr, None)): + getattr(baymodel, self.baymodel_attr, None) is not None): value = getattr(baymodel, self.baymodel_attr) elif (self.bay_attr and - getattr(bay, self.bay_attr, None)): + getattr(bay, self.bay_attr, None) is not None): value = getattr(bay, self.bay_attr) elif self.required: kwargs = dict(heat_param=self.heat_param) raise exception.RequiredParameterNotProvided(**kwargs) - if value: + if value is not None: value = self.param_type(value) params[self.heat_param] = value @@ -481,6 +482,9 @@ class AtomicSwarmTemplateDefinition(BaseTemplateDefinition): def __init__(self): super(AtomicSwarmTemplateDefinition, self).__init__() + self.add_parameter('bay_uuid', + bay_attr='uuid', + param_type=str) self.add_parameter('number_of_nodes', bay_attr='node_count', param_type=str) @@ -489,6 +493,9 @@ class AtomicSwarmTemplateDefinition(BaseTemplateDefinition): self.add_parameter('external_network', baymodel_attr='external_network_id', required=True) + self.add_parameter('insecure', + baymodel_attr='insecure', + required=True) self.add_output('swarm_master', bay_attr='api_address') self.add_output('swarm_nodes_external', @@ -519,6 +526,12 @@ class AtomicSwarmTemplateDefinition(BaseTemplateDefinition): def get_params(self, context, baymodel, bay, **kwargs): extra_params = kwargs.pop('extra_params', {}) extra_params['discovery_url'] = self.get_discovery_url(bay) + # HACK(apmelton) - This uses the user's bearer token, ideally + # it should be replaced with an actual trust token with only + # access to do what the template needs it to do. + extra_params['user_token'] = context.auth_token + osc = clients.OpenStackClients(context) + extra_params['magnum_url'] = osc.magnum_url() return super(AtomicSwarmTemplateDefinition, self).get_params(context, baymodel, bay, diff --git a/magnum/db/sqlalchemy/alembic/versions/1d045384b966_add_insecure_baymodel_attr.py b/magnum/db/sqlalchemy/alembic/versions/1d045384b966_add_insecure_baymodel_attr.py new file mode 100644 index 0000000000..c971bfaca8 --- /dev/null +++ b/magnum/db/sqlalchemy/alembic/versions/1d045384b966_add_insecure_baymodel_attr.py @@ -0,0 +1,34 @@ +# 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. +"""add-insecure-baymodel-attr + +Revision ID: 1d045384b966 +Revises: 1481f5b560dd +Create Date: 2015-09-23 18:17:10.195121 + +""" + +# revision identifiers, used by Alembic. +revision = '1d045384b966' +down_revision = '1481f5b560dd' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + insecure_column = sa.Column('insecure', sa.Boolean(), default=False) + op.add_column('baymodel', insecure_column) + baymodel = sa.sql.table('baymodel', insecure_column) + op.execute( + baymodel.update().values({'insecure': True}) + ) diff --git a/magnum/db/sqlalchemy/models.py b/magnum/db/sqlalchemy/models.py index 32147c45ad..a51d72911f 100644 --- a/magnum/db/sqlalchemy/models.py +++ b/magnum/db/sqlalchemy/models.py @@ -172,6 +172,7 @@ class BayModel(Base): no_proxy = Column(String(255)) registry_enabled = Column(Boolean, default=False) labels = Column(JSONEncodedDict) + insecure = Column(Boolean, default=False) class Container(Base): diff --git a/magnum/objects/baymodel.py b/magnum/objects/baymodel.py index ca015adf54..168ff5cbbe 100644 --- a/magnum/objects/baymodel.py +++ b/magnum/objects/baymodel.py @@ -25,7 +25,8 @@ class BayModel(base.MagnumPersistentObject, base.MagnumObject, # Version 1.1: Add 'registry_enabled' field # Version 1.2: Added 'network_driver' field # Version 1.3: Added 'labels' attribute - VERSION = '1.3' + # Version 1.4: Added 'insecure' attribute + VERSION = '1.4' dbapi = dbapi.get_instance() @@ -52,7 +53,8 @@ class BayModel(base.MagnumPersistentObject, base.MagnumObject, 'https_proxy': fields.StringField(nullable=True), 'no_proxy': fields.StringField(nullable=True), 'registry_enabled': fields.BooleanField(default=False), - 'labels': fields.DictOfStringsField(nullable=True) + 'labels': fields.DictOfStringsField(nullable=True), + 'insecure': fields.BooleanField(default=False), } @staticmethod diff --git a/magnum/templates/docker-swarm/fragments/make_cert.py b/magnum/templates/docker-swarm/fragments/make_cert.py new file mode 100644 index 0000000000..14b6adf0bc --- /dev/null +++ b/magnum/templates/docker-swarm/fragments/make_cert.py @@ -0,0 +1,145 @@ +#!/usr/bin/python + +# Copyright 2015 Rackspace, Inc. +# +# 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 json +import os +import subprocess + +import requests + +HEAT_PARAMS_PATH = '/etc/sysconfig/heat-params' +PUBLIC_IP_URL = 'http://169.254.169.254/latest/meta-data/public-ipv4' +CERT_DIR = '/etc/docker' +CERT_CONF_DIR = '%s/conf' % CERT_DIR +CA_CERT_PATH = '%s/ca.crt' % CERT_DIR +SERVER_CONF_PATH = '%s/server.conf' % CERT_CONF_DIR +SERVER_KEY_PATH = '%s/server.key' % CERT_DIR +SERVER_CSR_PATH = '%s/server.csr' % CERT_DIR +SERVER_CERT_PATH = '%s/server.crt' % CERT_DIR + +CSR_CONFIG_TEMPLATE = """ +[req] +distinguished_name = req_distinguished_name +req_extensions = req_ext +x509_extensions = req_ext +prompt = no +copy_extensions = copyall +[req_distinguished_name] +CN = swarm.invalid +[req_ext] +subjectAltName = %(subject_alt_names)s +extendedKeyUsage = clientAuth,serverAuth +""" + + +def _parse_config_value(value): + parsed_value = value + if parsed_value[-1] == '\n': + parsed_value = parsed_value[:-1] + return parsed_value[1:-1] + + +def load_config(): + config = dict() + with open(HEAT_PARAMS_PATH, 'r') as fp: + for line in fp.readlines(): + key, value = line.split('=', 1) + config[key] = _parse_config_value(value) + return config + + +def create_dirs(): + os.makedirs(CERT_CONF_DIR) + + +def _get_public_ip(): + return requests.get(PUBLIC_IP_URL).text + + +def _build_subject_alt_names(config): + subject_alt_names = [ + 'IP:%s' % _get_public_ip(), + 'IP:%s' % config['SWARM_NODE_IP'], + 'IP:127.0.0.1' + ] + return ','.join(subject_alt_names) + + +def write_ca_cert(config): + bay_cert_url = '%s/certificates/%s' % (config['MAGNUM_URL'], + config['BAY_UUID']) + headers = {'X-Auth-Token': config['USER_TOKEN']} + ca_cert_resp = requests.get(bay_cert_url, + headers=headers) + + with open(CA_CERT_PATH, 'w') as fp: + fp.write(ca_cert_resp.json()['pem']) + + +def write_server_key(): + subprocess.call(['openssl', 'genrsa', + '-out', SERVER_KEY_PATH, + '4096']) + + +def _write_csr_config(config): + with open(SERVER_CONF_PATH, 'w') as fp: + params = { + 'subject_alt_names': _build_subject_alt_names(config) + } + fp.write(CSR_CONFIG_TEMPLATE % params) + + +def create_server_csr(config): + _write_csr_config(config) + subprocess.call(['openssl', 'req', '-new', + '-days', '1000', + '-key', SERVER_KEY_PATH, + '-out', SERVER_CSR_PATH, + '-reqexts', 'req_ext', + '-extensions', 'req_ext', + '-config', SERVER_CONF_PATH]) + + with open(SERVER_CSR_PATH, 'r') as fp: + return {'bay_uuid': config['BAY_UUID'], 'csr': fp.read()} + + +def write_server_cert(config, csr_req): + cert_url = '%s/certificates' % config['MAGNUM_URL'] + headers = { + 'Content-Type': 'application/json', + 'X-Auth-Token': config['USER_TOKEN'] + } + csr_resp = requests.post(cert_url, + data=json.dumps(csr_req), + headers=headers) + + with open(SERVER_CERT_PATH, 'w') as fp: + fp.write(csr_resp.json()['pem']) + + +def main(): + config = load_config() + if config['INSECURE'] == 'False': + create_dirs() + write_ca_cert(config) + write_server_key() + csr_req = create_server_csr(config) + write_server_cert(config, csr_req) + + +if __name__ == '__main__': + main() diff --git a/magnum/templates/docker-swarm/fragments/write-docker-service.sh b/magnum/templates/docker-swarm/fragments/write-docker-service.sh new file mode 100644 index 0000000000..7fe29a00b4 --- /dev/null +++ b/magnum/templates/docker-swarm/fragments/write-docker-service.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +. /etc/sysconfig/heat-params + +mkdir -p /etc/systemd/system/docker.service.d + +cat > /etc/systemd/system/docker.service << END_SERVICE_TOP +[Unit] +Description=Docker Application Container Engine +Documentation=http://docs.docker.com +After=network.target docker.socket +Requires=docker.socket + +[Service] +Type=notify +EnvironmentFile=-/etc/sysconfig/docker +EnvironmentFile=-/etc/sysconfig/docker-storage +EnvironmentFile=-/etc/sysconfig/docker-network +ExecStart=/usr/bin/docker -d -H fd:// \\ + -H tcp://0.0.0.0:2375 \\ +END_SERVICE_TOP + +if [ $INSECURE == 'False' ]; then + +cat >> /etc/systemd/system/docker.service << END_TLS + --tls \\ + --tlsverify \\ + --tlscacert="/etc/docker/ca.crt" \\ + --tlskey="/etc/docker/server.key" \\ + --tlscert="/etc/docker/server.crt" \\ +END_TLS + +fi + +cat >> /etc/systemd/system/docker.service << END_SERVICE_BOTTOM + \$OPTIONS \\ + \$DOCKER_STORAGE_OPTIONS \\ + \$DOCKER_NETWORK_OPTIONS \\ + \$INSECURE_REGISTRY +LimitNOFILE=1048576 +LimitNPROC=1048576 +LimitCORE=infinity +MountFlags=slave + +[Install] +WantedBy=multi-user.target +END_SERVICE_BOTTOM + +chown root:root /etc/systemd/system/docker.service +chmod 644 /etc/systemd/system/docker.service diff --git a/magnum/templates/docker-swarm/fragments/write-docker-service.yaml b/magnum/templates/docker-swarm/fragments/write-docker-service.yaml deleted file mode 100644 index 6444e70b03..0000000000 --- a/magnum/templates/docker-swarm/fragments/write-docker-service.yaml +++ /dev/null @@ -1,32 +0,0 @@ -#cloud-config -merge_how: dict(recurse_array)+list(append) -bootcmd: - - mkdir -p /etc/systemd/system/docker.service.d -write_files: - - path: /etc/systemd/system/docker.service - owner: "root:root" - permissions: "0644" - content: | - [Unit] - Description=Docker Application Container Engine - Documentation=http://docs.docker.com - After=network.target docker.socket - Requires=docker.socket - - [Service] - Type=notify - EnvironmentFile=-/etc/sysconfig/docker - EnvironmentFile=-/etc/sysconfig/docker-storage - EnvironmentFile=-/etc/sysconfig/docker-network - ExecStart=/usr/bin/docker -d -H fd:// \ - $OPTIONS \ - $DOCKER_STORAGE_OPTIONS \ - $DOCKER_NETWORK_OPTIONS \ - $INSECURE_REGISTRY - LimitNOFILE=1048576 - LimitNPROC=1048576 - LimitCORE=infinity - MountFlags=slave - - [Install] - WantedBy=multi-user.target diff --git a/magnum/templates/docker-swarm/fragments/write-docker-tcp-socket.yaml b/magnum/templates/docker-swarm/fragments/write-docker-tcp-socket.yaml deleted file mode 100644 index 2ba057b949..0000000000 --- a/magnum/templates/docker-swarm/fragments/write-docker-tcp-socket.yaml +++ /dev/null @@ -1,18 +0,0 @@ -#cloud-config -merge_how: dict(recurse_array)+list(append) -write_files: - - path: /etc/systemd/system/docker-tcp.socket - owner: "root:root" - permissions: "0644" - content: | - [Unit] - Description=Docker Socket for the API - PartOf=docker.service - - [Socket] - ListenStream=2375 - BindIPv6Only=both - Service=docker.service - - [Install] - WantedBy=sockets.target diff --git a/magnum/templates/docker-swarm/fragments/write-heat-params.yaml b/magnum/templates/docker-swarm/fragments/write-heat-params.yaml index 61c7f68c45..0c83f587e2 100644 --- a/magnum/templates/docker-swarm/fragments/write-heat-params.yaml +++ b/magnum/templates/docker-swarm/fragments/write-heat-params.yaml @@ -10,3 +10,8 @@ write_files: HTTPS_PROXY="$HTTPS_PROXY" NO_PROXY="$NO_PROXY" SWARM_MASTER_IP="$SWARM_MASTER_IP" + SWARM_NODE_IP="$SWARM_NODE_IP" + BAY_UUID="$BAY_UUID" + USER_TOKEN="$USER_TOKEN" + MAGNUM_URL="$MAGNUM_URL" + INSECURE="$INSECURE" diff --git a/magnum/templates/docker-swarm/fragments/write-swarm-master-service.sh b/magnum/templates/docker-swarm/fragments/write-swarm-master-service.sh new file mode 100644 index 0000000000..6a3920e5d8 --- /dev/null +++ b/magnum/templates/docker-swarm/fragments/write-swarm-master-service.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +cat > /etc/systemd/system/swarm-manager.service << END_SERVICE_TOP +[Unit] +Description=Swarm Manager +After=docker.service +Requires=docker.service + +[Service] +TimeoutStartSec=0 +ExecStartPre=-/usr/bin/docker kill swarm-manager +ExecStartPre=-/usr/bin/docker rm swarm-manager +ExecStartPre=/usr/bin/docker pull swarm:0.2.0 +#TODO: roll-back from swarm:0.2.0 to swarm if atomic image can work with latest swarm image +ExecStart=/usr/bin/docker run --name swarm-manager \\ + -v /etc/docker:/etc/docker \\ + -p 2376:2375 \\ + -e http_proxy=$HTTP_PROXY \\ + -e https_proxy=$HTTPS_PROXY \\ + -e no_proxy=$NO_PROXY \\ + swarm:0.2.0 \\ + manage -H tcp://0.0.0.0:2375 \\ +END_SERVICE_TOP + +if [ $INSECURE = 'False' ]; then + +cat >> /etc/systemd/system/swarm-manager.service << END_TLS + --tls \\ + --tlsverify \\ + --tlscacert=/etc/docker/ca.crt \\ + --tlskey=/etc/docker/server.key \\ + --tlscert=/etc/docker/server.crt \\ +END_TLS + +fi + +cat >> /etc/systemd/system/swarm-manager.service << END_SERVICE_BOTTOM + $DISCOVERY_URL +ExecStop=/usr/bin/docker stop swarm-manager +ExecStartPost=/usr/bin/curl -sf -X PUT -H 'Content-Type: application/json' \\ + --data-binary '{"Status": "SUCCESS", "Reason": "Setup complete", "Data": "OK", "UniqueId": "00000"}' \\ + "$WAIT_HANDLE" + +[Install] +WantedBy=multi-user.target +END_SERVICE_BOTTOM + +chown root:root /etc/systemd/system/swarm-manager.service +chmod 644 /etc/systemd/system/swarm-manager.service diff --git a/magnum/templates/docker-swarm/fragments/write-swarm-master-service.yaml b/magnum/templates/docker-swarm/fragments/write-swarm-master-service.yaml deleted file mode 100644 index 0fe438a23c..0000000000 --- a/magnum/templates/docker-swarm/fragments/write-swarm-master-service.yaml +++ /dev/null @@ -1,26 +0,0 @@ -#cloud-config -merge_how: dict(recurse_array)+list(append) -write_files: - - path: /etc/systemd/system/swarm-manager.service - owner: "root:root" - permissions: "0644" - content: | - [Unit] - Description=Swarm Manager - After=docker.service - Requires=docker.service - - [Service] - TimeoutStartSec=0 - ExecStartPre=-/usr/bin/docker kill swarm-manager - ExecStartPre=-/usr/bin/docker rm swarm-manager - ExecStartPre=/usr/bin/docker pull swarm:0.2.0 - #TODO: roll-back from swarm:0.2.0 to swarm if atomic image can work with latest swarm image - ExecStart=/usr/bin/docker run -e http_proxy=$HTTP_PROXY -e https_proxy=$HTTPS_PROXY -e no_proxy=$NO_PROXY --name swarm-manager -p 2376:2375 swarm:0.2.0 manage -H tcp://0.0.0.0:2375 $DISCOVERY_URL - ExecStop=/usr/bin/docker stop swarm-manager - ExecStartPost=/usr/bin/curl -sf -X PUT -H 'Content-Type: application/json' \ - --data-binary '{"Status": "SUCCESS", "Reason": "Setup complete", "Data": "OK", "UniqueId": "00000"}' \ - "$WAIT_HANDLE" - - [Install] - WantedBy=multi-user.target diff --git a/magnum/templates/docker-swarm/swarm.yaml b/magnum/templates/docker-swarm/swarm.yaml index 3204624deb..caaac18e20 100644 --- a/magnum/templates/docker-swarm/swarm.yaml +++ b/magnum/templates/docker-swarm/swarm.yaml @@ -25,6 +25,18 @@ parameters: type: string description: url provided for node discovery + user_token: + type: string + description: token used for communicating back to Magnum for TLS certs + + bay_uuid: + type: string + description: identifier for the bay this template is generating + + magnum_url: + type: string + description: endpoint to retrieve TLS certs from + # # OPTIONAL PARAMETERS # @@ -68,6 +80,11 @@ parameters: description: network range for fixed ip network default: "10.0.0.0/24" + insecure: + type: boolean + description: whether or not to enable TLS + default: False + resources: master_wait_handle: @@ -171,6 +188,11 @@ resources: "$HTTPS_PROXY": {get_param: https_proxy} "$NO_PROXY": {get_param: no_proxy} "$SWARM_MASTER_IP": {get_attr: [swarm_master_eth0, fixed_ips, 0, ip_address]} + "$SWARM_NODE_IP": {get_attr: [swarm_master_eth0, fixed_ips, 0, ip_address]} + "$BAY_UUID": {get_param: bay_uuid} + "$USER_TOKEN": {get_param: user_token} + "$MAGNUM_URL": {get_param: magnum_url} + "$INSECURE": {get_param: insecure} remove_docker_key: type: "OS::Heat::SoftwareConfig" @@ -178,11 +200,17 @@ resources: group: ungrouped config: {get_file: fragments/remove-docker-key.sh} + make_cert: + type: "OS::Heat::SoftwareConfig" + properties: + group: ungrouped + config: {get_file: fragments/make_cert.py} + write_docker_service: type: "OS::Heat::SoftwareConfig" properties: group: ungrouped - config: {get_file: fragments/write-docker-service.yaml} + config: {get_file: fragments/write-docker-service.sh} write_docker_socket: type: "OS::Heat::SoftwareConfig" @@ -190,12 +218,6 @@ resources: group: ungrouped config: {get_file: fragments/write-docker-socket.yaml} - write_docker_tcp_socket: - type: "OS::Heat::SoftwareConfig" - properties: - group: ungrouped - config: {get_file: fragments/write-docker-tcp-socket.yaml} - write_swarm_agent_service: type: "OS::Heat::SoftwareConfig" properties: @@ -217,13 +239,14 @@ resources: group: ungrouped config: str_replace: - template: {get_file: fragments/write-swarm-master-service.yaml} + template: {get_file: fragments/write-swarm-master-service.sh} params: "$DISCOVERY_URL": {get_param: discovery_url} "$WAIT_HANDLE": {get_resource: master_wait_handle} "$HTTP_PROXY": {get_param: http_proxy} "$HTTPS_PROXY": {get_param: https_proxy} "$NO_PROXY": {get_param: no_proxy} + "$INSECURE": {get_param: insecure} enable_services: type: "OS::Heat::SoftwareConfig" @@ -261,9 +284,9 @@ resources: - config: {get_resource: remove_docker_key} - config: {get_resource: write_heat_params} - config: {get_resource: add_proxy} + - config: {get_resource: make_cert} - config: {get_resource: write_docker_service} - config: {get_resource: write_docker_socket} - - config: {get_resource: write_docker_tcp_socket} - config: {get_resource: write_swarm_agent_service} - config: {get_resource: write_swarm_master_service} - config: {get_resource: enable_services} @@ -333,6 +356,10 @@ resources: https_proxy: {get_param: https_proxy} no_proxy: {get_param: no_proxy} swarm_master_ip: {get_attr: [swarm_master_eth0, fixed_ips, 0, ip_address]} + bay_uuid: {get_param: bay_uuid} + user_token: {get_param: user_token} + magnum_url: {get_param: magnum_url} + insecure: {get_param: insecure} outputs: diff --git a/magnum/templates/docker-swarm/swarmnode.yaml b/magnum/templates/docker-swarm/swarmnode.yaml index 2285934488..f10e723750 100644 --- a/magnum/templates/docker-swarm/swarmnode.yaml +++ b/magnum/templates/docker-swarm/swarmnode.yaml @@ -57,6 +57,22 @@ parameters: type: string description: swarm master's ip address + user_token: + type: string + description: token used for communicating back to Magnum for TLS certs + + bay_uuid: + type: string + description: identifier for the bay this template is generating + + magnum_url: + type: string + description: endpoint to retrieve TLS certs from + + insecure: + type: boolean + description: whether or not to disable TLS + resources: node_wait_handle: @@ -119,6 +135,11 @@ resources: "$HTTPS_PROXY": {get_param: https_proxy} "$NO_PROXY": {get_param: no_proxy} "$SWARM_MASTER_IP": {get_param: swarm_master_ip} + "$SWARM_NODE_IP": {get_attr: [swarm_node_eth0, fixed_ips, 0, ip_address]} + "$BAY_UUID": {get_param: bay_uuid} + "$USER_TOKEN": {get_param: user_token} + "$MAGNUM_URL": {get_param: magnum_url} + "$INSECURE": {get_param: insecure} remove_docker_key: type: "OS::Heat::SoftwareConfig" @@ -126,11 +147,17 @@ resources: group: ungrouped config: {get_file: fragments/remove-docker-key.sh} + make_cert: + type: "OS::Heat::SoftwareConfig" + properties: + group: ungrouped + config: {get_file: fragments/make_cert.py} + write_docker_service: type: "OS::Heat::SoftwareConfig" properties: group: ungrouped - config: {get_file: fragments/write-docker-service.yaml} + config: {get_file: fragments/write-docker-service.sh} write_docker_socket: type: "OS::Heat::SoftwareConfig" @@ -138,12 +165,6 @@ resources: group: ungrouped config: {get_file: fragments/write-docker-socket.yaml} - write_docker_tcp_socket: - type: "OS::Heat::SoftwareConfig" - properties: - group: ungrouped - config: {get_file: fragments/write-docker-tcp-socket.yaml} - write_swarm_agent_service: type: "OS::Heat::SoftwareConfig" properties: @@ -194,11 +215,11 @@ resources: - config: {get_resource: disable_selinux} - config: {get_resource: remove_docker_key} - config: {get_resource: write_heat_params} + - config: {get_resource: make_cert} - config: {get_resource: add_proxy} - config: {get_resource: write_swarm_agent_service} - config: {get_resource: write_docker_service} - config: {get_resource: write_docker_socket} - - config: {get_resource: write_docker_tcp_socket} - config: {get_resource: enable_services} - config: {get_resource: cfn_signal} diff --git a/magnum/tests/unit/conductor/handlers/test_bay_conductor.py b/magnum/tests/unit/conductor/handlers/test_bay_conductor.py index df3f02f65b..011babc2bf 100644 --- a/magnum/tests/unit/conductor/handlers/test_bay_conductor.py +++ b/magnum/tests/unit/conductor/handlers/test_bay_conductor.py @@ -11,6 +11,7 @@ # 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 uuid from heatclient import exc from oslo_service import loopingcall @@ -795,18 +796,51 @@ class TestHandler(db_base.DbTestCase): bay = objects.Bay.get(self.context, self.bay.uuid) self.assertEqual(bay.node_count, 1) - @patch('magnum.conductor.handlers.common.cert_manager.' - 'generate_certificates_to_bay') + @patch('magnum.conductor.handlers.bay_conductor.HeatPoller') + @patch('magnum.conductor.handlers.bay_conductor.cert_manager') + @patch('magnum.conductor.handlers.bay_conductor._create_stack') + @patch('magnum.conductor.handlers.bay_conductor.uuid') + @patch('magnum.common.clients.OpenStackClients') + def test_create(self, mock_openstack_client_class, mock_uuid, + mock_create_stack, mock_cert_manager, + mock_heat_poller_class): + timeout = 15 + test_uuid = uuid.uuid4() + mock_uuid.uuid4.return_value = test_uuid + mock_poller = mock.MagicMock() + mock_poller.poll_and_check.return_value = loopingcall.LoopingCallDone() + mock_heat_poller_class.return_value = mock_poller + mock_openstack_client_class.return_value = mock.sentinel.osc + + def create_stack_side_effect(context, osc, bay, timeout): + self.assertEqual(bay.uuid, str(test_uuid)) + return {'stack': {'id': 'stack-id'}} + + mock_create_stack.side_effect = create_stack_side_effect + + self.handler.bay_create(self.context, + self.bay, timeout) + + mock_create_stack.assert_called_once_with(self.context, + mock.sentinel.osc, + self.bay, timeout) + mock_cert_manager.generate_certificates_to_bay.assert_called_once_with( + self.bay) + + @patch('magnum.conductor.handlers.bay_conductor.cert_manager') @patch('magnum.conductor.handlers.bay_conductor._create_stack') @patch('magnum.common.clients.OpenStackClients') - def test_create(self, mock_openstack_client_class, mock_create_stack, - mock_generate_certificates): + def test_create_handles_bad_request(self, mock_openstack_client_class, + mock_create_stack, + mock_cert_manager): mock_create_stack.side_effect = exc.HTTPBadRequest timeout = 15 self.assertRaises(exception.InvalidParameterValue, self.handler.bay_create, self.context, self.bay, timeout) - mock_generate_certificates.assert_called_once_with(self.bay) + mock_cert_manager.generate_certificates_to_bay.assert_called_once_with( + self.bay) + mock_cert_manager.delete_certificates_from_bay(self.bay) @patch('magnum.common.clients.OpenStackClients') def test_bay_delete(self, mock_openstack_client_class): @@ -833,7 +867,8 @@ class TestBayConductorWithSwarm(base.TestCase): 'coe': 'swarm', 'http_proxy': 'http_proxy', 'https_proxy': 'https_proxy', - 'no_proxy': 'no_proxy' + 'no_proxy': 'no_proxy', + 'insecure': False } self.bay_dict = { 'id': 1, @@ -846,6 +881,12 @@ class TestBayConductorWithSwarm(base.TestCase): 'node_count': 1, 'discovery_url': 'token://39987da72f8386e0d0225ae8929e7cb4', } + osc_patcher = mock.patch('magnum.common.clients.OpenStackClients') + self.mock_osc_class = osc_patcher.start() + self.addCleanup(osc_patcher.stop) + self.mock_osc = mock.MagicMock() + self.mock_osc.magnum_url.return_value = 'http://127.0.0.1:9511/v1' + self.mock_osc_class.return_value = self.mock_osc @patch('magnum.objects.BayModel.get_by_uuid') def test_extract_template_definition_all_values( @@ -870,7 +911,12 @@ class TestBayConductorWithSwarm(base.TestCase): 'discovery_url': 'token://39987da72f8386e0d0225ae8929e7cb4', 'http_proxy': 'http_proxy', 'https_proxy': 'https_proxy', - 'no_proxy': 'no_proxy' + 'no_proxy': 'no_proxy', + 'user_token': self.context.auth_token, + 'bay_uuid': 'some_uuid', + 'magnum_url': self.mock_osc.magnum_url.return_value, + 'insecure': False + } self.assertEqual(expected, definition) @@ -901,7 +947,11 @@ class TestBayConductorWithSwarm(base.TestCase): 'ssh_key_name': 'keypair_id', 'external_network': 'external_network_id', 'number_of_nodes': '1', - 'discovery_url': 'test_discovery' + 'discovery_url': 'test_discovery', + 'user_token': self.context.auth_token, + 'bay_uuid': 'some_uuid', + 'magnum_url': self.mock_osc.magnum_url.return_value, + 'insecure': False } self.assertEqual(expected, definition) diff --git a/magnum/tests/unit/db/utils.py b/magnum/tests/unit/db/utils.py index 4fff74cb23..9ccacbd172 100644 --- a/magnum/tests/unit/db/utils.py +++ b/magnum/tests/unit/db/utils.py @@ -52,7 +52,8 @@ def get_test_baymodel(**kw): 'http_proxy': kw.get('http_proxy', 'fake_http_proxy'), 'https_proxy': kw.get('https_proxy', 'fake_https_proxy'), 'no_proxy': kw.get('no_proxy', 'fake_no_proxy'), - 'registry_enabled': kw.get('registry_enabled', False) + 'registry_enabled': kw.get('registry_enabled', False), + 'insecure': kw.get('insecure', False) } diff --git a/magnum/tests/unit/objects/test_objects.py b/magnum/tests/unit/objects/test_objects.py index 2cac215055..dde6c75698 100644 --- a/magnum/tests/unit/objects/test_objects.py +++ b/magnum/tests/unit/objects/test_objects.py @@ -426,7 +426,7 @@ class _TestObject(object): object_data = { 'Bay': '1.0-35edde13ad178e9419e7ea8b6d580bcd', 'BayLock': '1.0-7d1eb08cf2070523bd210369c7a2e076', - 'BayModel': '1.3-369d7b7f05720780ae4f6c5d983e8c3e', + 'BayModel': '1.4-68d7979ff1d81f948180fb620e6f84c7', 'Certificate': '1.0-2aff667971b85c1edf8d15684fd7d5e2', 'Container': '1.0-e12affbba5f8a748882a3ae98aced282', 'MyObj': '1.0-b43567e512438205e32f4e95ca616697',