Allow user-data on amphora creation

Currently, Amphora configuration data is being sent as personality
files as considered by Nova and some providers have limitations
and must use cloud-init user-data.

This patch introduces a new config option to enable user-data.
If enabled the files that were built, such as the amphora config
and certificates for the agent, will be templated into a cloud-init
user-data script that loads the files as expected. After this we
need to restart the agent as cloud-init happens at a higher level
than service scripts. This does increase the boot time.

This is configurable so there is no impact if it's not needed.

Change-Id: I60fa87722302eee9d3d1fd6ff1b5b5b697a2406e
Closes-Bug: #1541231
This commit is contained in:
ptoohill1 2016-02-03 01:27:37 -06:00
parent ca8c263a3d
commit 025ec0024b
11 changed files with 223 additions and 11 deletions

View File

@ -162,6 +162,7 @@
#
# Load balancer topology options are SINGLE, ACTIVE_STANDBY
# loadbalancer_topology = SINGLE
# user_data_config_drive = False
[task_flow]
# engine = serial

View File

@ -240,7 +240,12 @@ controller_worker_opts = [
choices=constants.SUPPORTED_LB_TOPOLOGIES,
help=_('Load balancer topology configuration. '
'SINGLE - One amphora per load balancer. '
'ACTIVE_STANDBY - Two amphora per load balancer.'))
'ACTIVE_STANDBY - Two amphora per load balancer.')),
cfg.BoolOpt('user_data_config_drive', default=False,
help=_('If True, build cloud-init user-data that is passed '
'to the config drive on Amphora boot instead of '
'personality files. If False, utilize personality '
'files.'))
]
task_flow_opts = [

View File

@ -210,8 +210,11 @@ VRRP_PROTOCOL_NUM = 112
AUTH_HEADER_PROTOCOL_NUMBER = 51
TEMPLATES = '/templates'
AGENT_API_TEMPLATES = '/templates'
AGENT_CONF_TEMPLATE = 'amphora_agent_conf.template'
USER_DATA_CONFIG_DRIVE_TEMPLATE = 'user_data_config_drive.template'
OPEN = 'OPEN'
FULL = 'FULL'

View File

View File

@ -0,0 +1,34 @@
{# Copyright 2016 Rackspace
#
# 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.
-#}
#cloud-config
# vim: syntax=yaml
#
# This configuration with take user-data dict and build a cloud-init
# script utilizing the write_files module. The user-data dict should be a
# Key Value pair where the Key is the path to store the file and the Value
# is the data to store at that location
#
# Example:
# {'/root/path/to/file.cfg': 'I'm a file, write things in me'}
write_files:
{%- for key, value in user_data.items() %}
- path: {{ key }}
content: |
{{ value|indent(8) }}
{%- endfor -%}
{# restart agent now that configurations are in place #}
runcmd:
- service amphora-agent restart

View File

@ -0,0 +1,41 @@
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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 os
import jinja2
from octavia.common.config import cfg
from octavia.common import constants
CONF = cfg.CONF
CONF.import_group('amphora_agent', 'octavia.common.config')
CONF.import_group('haproxy_amphora', 'octavia.common.config')
CONF.import_group('health_manager', 'octavia.common.config')
TEMPLATES_DIR = (os.path.dirname(os.path.realpath(__file__)) +
constants.TEMPLATES + '/')
class UserDataJinjaCfg(object):
def __init__(self):
template_loader = jinja2.FileSystemLoader(searchpath=os.path.dirname(
TEMPLATES_DIR))
jinja_env = jinja2.Environment(loader=template_loader)
self.agent_template = jinja_env.get_template(
constants.USER_DATA_CONFIG_DRIVE_TEMPLATE)
def build_user_data_config(self, user_data):
return self.agent_template.render(user_data=user_data)

View File

@ -25,6 +25,7 @@ from taskflow.types import failure
from octavia.amphorae.backends.agent import agent_jinja_cfg
from octavia.common import constants
from octavia.common import exceptions
from octavia.common.jinja import user_data_jinja_cfg
from octavia.i18n import _LE, _LW
CONF = cfg.CONF
@ -54,8 +55,10 @@ class ComputeCreate(BaseComputeTask):
"""
ports = ports or []
config_drive_files = config_drive_files or {}
user_data = None
LOG.debug("Compute create execute for amphora with id %s", amphora_id)
user_data_config_drive = CONF.controller_worker.user_data_config_drive
ssh_access = CONF.controller_worker.amp_ssh_access_allowed
ssh_key = CONF.controller_worker.amp_ssh_key_name
key_name = None if not ssh_access else ssh_key
@ -64,6 +67,12 @@ class ComputeCreate(BaseComputeTask):
agent_cfg = agent_jinja_cfg.AgentJinjaTemplater()
config_drive_files['/etc/octavia/amphora-agent.conf'] = (
agent_cfg.build_agent_config(amphora_id))
if user_data_config_drive:
udtemplater = user_data_jinja_cfg.UserDataJinjaCfg()
user_data = udtemplater.build_user_data_config(
config_drive_files)
config_drive_files = None
compute_id = self.compute.build(
name="amphora-" + amphora_id,
amphora_flavor=CONF.controller_worker.amp_flavor_id,
@ -72,7 +81,8 @@ class ComputeCreate(BaseComputeTask):
sec_groups=CONF.controller_worker.amp_secgroup_list,
network_ids=[CONF.controller_worker.amp_network],
port_ids=[port.id for port in ports],
config_drive_files=config_drive_files)
config_drive_files=config_drive_files,
user_data=user_data)
LOG.debug("Server created with id: %s for amphora id: %s",
compute_id, amphora_id)
@ -109,12 +119,13 @@ class CertComputeCreate(ComputeCreate):
# load client certificate
with open(CONF.controller_worker.client_ca, 'r') as client_ca:
config_drive_files = {
# '/etc/octavia/octavia.conf'
'/etc/octavia/certs/server.pem': server_pem,
'/etc/octavia/certs/client_ca.pem': client_ca}
return super(CertComputeCreate, self).execute(
amphora_id, ports=ports, config_drive_files=config_drive_files)
ca = client_ca.read()
config_drive_files = {
# '/etc/octavia/octavia.conf'
'/etc/octavia/certs/server.pem': server_pem,
'/etc/octavia/certs/client_ca.pem': ca}
return super(CertComputeCreate, self).execute(
amphora_id, ports=ports, config_drive_files=config_drive_files)
class DeleteAmphoraeOnLoadBalancer(BaseComputeTask):

View File

@ -0,0 +1,55 @@
# Copyright 2016 Rackspace
#
# 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.
from octavia.common.jinja import user_data_jinja_cfg
import octavia.tests.unit.base as base
TEST_CONFIG = ('[DEFAULT]\n'
'debug = False\n'
'[haproxy_amphora]\n'
'base_cert_dir = /var/lib/octavia/certs\n')
EXPECTED_TEST_CONFIG = (' [DEFAULT]\n'
' debug = False\n'
' [haproxy_amphora]\n'
' base_cert_dir = /var/lib/octavia/certs\n')
BASE_CFG = ('#cloud-config\n'
'# vim: syntax=yaml\n'
'#\n'
'# This configuration with take user-data dict and '
'build a cloud-init\n'
'# script utilizing the write_files module. '
'The user-data dict should be a\n'
'# Key Value pair where the Key is the path to store the '
'file and the Value\n'
'# is the data to store at that location\n'
'#\n'
'# Example:\n'
'# {\'/root/path/to/file.cfg\': \'I\'m a file, '
'write things in me\'}\n'
'write_files:\n')
RUN_CMD = ('runcmd:\n'
'- service amphora-agent restart')
class TestUserDataJinjaCfg(base.TestCase):
def setUp(self):
super(TestUserDataJinjaCfg, self).setUp()
def test_build_user_data_config(self):
udc = user_data_jinja_cfg.UserDataJinjaCfg()
expected_config = (BASE_CFG +
'- path: /test/config/path\n'
' content: |\n' + EXPECTED_TEST_CONFIG + RUN_CMD)
ud_cfg = udc.build_user_data_config({'/test/config/path': TEST_CONFIG})
self.assertEqual(expected_config, ud_cfg)

View File

@ -97,7 +97,8 @@ class TestComputeTasks(base.TestCase):
network_ids=[AMP_NET],
port_ids=[PORT_ID],
config_drive_files={'/etc/octavia/'
'amphora-agent.conf': 'test_conf'})
'amphora-agent.conf': 'test_conf'},
user_data=None)
# Make sure it returns the expected compute_id
assert(compute_id == COMPUTE_ID)
@ -124,6 +125,65 @@ class TestComputeTasks(base.TestCase):
createcompute.revert(COMPUTE_ID, _amphora_mock.id)
@mock.patch('jinja2.Environment.get_template')
@mock.patch('octavia.amphorae.backends.agent.'
'agent_jinja_cfg.AgentJinjaTemplater.'
'build_agent_config', return_value='test_conf')
@mock.patch('octavia.common.jinja.'
'user_data_jinja_cfg.UserDataJinjaCfg.'
'build_user_data_config', return_value='test_conf')
@mock.patch('stevedore.driver.DriverManager.driver')
def test_compute_create_user_data(self, mock_driver,
mock_ud_conf, mock_conf, mock_jinja):
conf = oslo_fixture.Config(cfg.CONF)
conf.config(group="controller_worker", user_data_config_drive=True)
mock_ud_conf.return_value = 'test_ud_conf'
createcompute = compute_tasks.ComputeCreate()
mock_driver.build.return_value = COMPUTE_ID
# Test execute()
compute_id = createcompute.execute(_amphora_mock.id, ports=[_port])
# Validate that the build method was called properly
mock_driver.build.assert_called_once_with(
name="amphora-" + _amphora_mock.id,
amphora_flavor=AMP_FLAVOR_ID,
image_id=AMP_IMAGE_ID,
key_name=AMP_SSH_KEY_NAME,
sec_groups=AMP_SEC_GROUPS,
network_ids=[AMP_NET],
port_ids=[PORT_ID],
config_drive_files=None,
user_data='test_ud_conf')
# Make sure it returns the expected compute_id
assert(compute_id == COMPUTE_ID)
# Test that a build exception is raised
createcompute = compute_tasks.ComputeCreate()
self.assertRaises(TypeError,
createcompute.execute,
_amphora_mock, config_drive_files='test_cert')
# Test revert()
_amphora_mock.compute_id = COMPUTE_ID
createcompute = compute_tasks.ComputeCreate()
createcompute.revert(compute_id, _amphora_mock.id)
# Validate that the delete method was called properly
mock_driver.delete.assert_called_once_with(
COMPUTE_ID)
# Test that a delete exception is not raised
createcompute.revert(COMPUTE_ID, _amphora_mock.id)
conf = oslo_fixture.Config(cfg.CONF)
conf.config(group="controller_worker", user_data_config_drive=False)
@mock.patch('jinja2.Environment.get_template')
@mock.patch('octavia.amphorae.backends.agent.'
'agent_jinja_cfg.AgentJinjaTemplater.'
@ -151,7 +211,8 @@ class TestComputeTasks(base.TestCase):
network_ids=[AMP_NET],
port_ids=[PORT_ID],
config_drive_files={'/etc/octavia/'
'amphora-agent.conf': 'test_conf'})
'amphora-agent.conf': 'test_conf'},
user_data=None)
# Make sure it returns the expected compute_id
self.assertEqual(COMPUTE_ID, compute_id)
@ -203,9 +264,10 @@ class TestComputeTasks(base.TestCase):
sec_groups=AMP_SEC_GROUPS,
network_ids=[AMP_NET],
port_ids=[],
user_data=None,
config_drive_files={
'/etc/octavia/certs/server.pem': 'test_cert',
'/etc/octavia/certs/client_ca.pem': m.return_value,
'/etc/octavia/certs/client_ca.pem': 'test',
'/etc/octavia/amphora-agent.conf': 'test_conf'})
# Make sure it returns the expected compute_id