From 5719a6e05afb0fc32edf1f8c2d5530839dff5c04 Mon Sep 17 00:00:00 2001 From: Andrew Melton Date: Thu, 13 Aug 2015 15:29:12 +0000 Subject: [PATCH] Add TLS to Docker-Swarm Template This patch adds TLS at two key places. The first is on the Docker daemon itself, which secures communication between the Swarm manager service and the Docker Daemon. The second is on the Swarm manager, which secures communication between the Magnum conductor and the Swarm manager. TLS can be disabled with the 'insecure' attribute on BayModel. It is enabled by default on new BayModels, but is set to False on existing BayModels as not to break how they currently function. Partial-Implements: blueprint secure-docker Change-Id: Ic88edf4b2e0005f6aa0a6df33b94ff275a5623d2 --- doc/source/tls.rst | 106 +++++++++++++ magnum/api/controllers/v1/baymodel.py | 3 + magnum/conductor/handlers/bay_conductor.py | 2 + magnum/conductor/template_definition.py | 19 ++- ...1d045384b966_add_insecure_baymodel_attr.py | 34 ++++ magnum/db/sqlalchemy/models.py | 1 + magnum/objects/baymodel.py | 6 +- .../docker-swarm/fragments/make_cert.py | 145 ++++++++++++++++++ .../fragments/write-docker-service.sh | 50 ++++++ .../fragments/write-docker-service.yaml | 32 ---- .../fragments/write-docker-tcp-socket.yaml | 18 --- .../fragments/write-heat-params.yaml | 5 + .../fragments/write-swarm-master-service.sh | 49 ++++++ .../fragments/write-swarm-master-service.yaml | 26 ---- magnum/templates/docker-swarm/swarm.yaml | 45 ++++-- magnum/templates/docker-swarm/swarmnode.yaml | 37 ++++- .../conductor/handlers/test_bay_conductor.py | 66 +++++++- magnum/tests/unit/db/utils.py | 3 +- magnum/tests/unit/objects/test_objects.py | 2 +- 19 files changed, 541 insertions(+), 108 deletions(-) create mode 100644 doc/source/tls.rst create mode 100644 magnum/db/sqlalchemy/alembic/versions/1d045384b966_add_insecure_baymodel_attr.py create mode 100644 magnum/templates/docker-swarm/fragments/make_cert.py create mode 100644 magnum/templates/docker-swarm/fragments/write-docker-service.sh delete mode 100644 magnum/templates/docker-swarm/fragments/write-docker-service.yaml delete mode 100644 magnum/templates/docker-swarm/fragments/write-docker-tcp-socket.yaml create mode 100644 magnum/templates/docker-swarm/fragments/write-swarm-master-service.sh delete mode 100644 magnum/templates/docker-swarm/fragments/write-swarm-master-service.yaml 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 c8485d875e..777560983e 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 _ @@ -105,16 +106,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 @@ -471,6 +472,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) @@ -479,6 +483,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', @@ -509,6 +516,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 c96adcf812..aa402cb8de 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',