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
This commit is contained in:
Andrew Melton 2015-08-13 15:29:12 +00:00
parent 07ec627ca0
commit 5719a6e05a
19 changed files with 541 additions and 108 deletions

106
doc/source/tls.rst Normal file
View File

@ -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 <https://www.feistyduck.com/books/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 <bay-id> --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 <bay-id> > ca.crt
Once you have all of these pieces, you can configure your native client. Below
is an example for Docker.
::
docker -H tcp://<bay_api_address>:2376 --tls --tlsverify \
--tlscacert ca.crt \
--tlskey client.key \
--tlscert client.crt
info

View File

@ -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:

View File

@ -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:

View File

@ -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,

View File

@ -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})
)

View File

@ -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):

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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}

View File

@ -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)

View File

@ -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)
}

View File

@ -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',