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:
parent
07ec627ca0
commit
5719a6e05a
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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})
|
||||
)
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue