Add amphora agent configuration update admin API

This patch adds a new admin API that updates an amphora's agent
configuration.

Change-Id: I41ce6843fb53fa21ab84e5b1d0734e70380d716a
This commit is contained in:
Michael Johnson
2019-01-23 17:26:22 -08:00
parent fae5b05980
commit 52ffdd16a6
16 changed files with 343 additions and 3 deletions

View File

@@ -215,6 +215,52 @@ Response Example
.. literalinclude:: examples/amphora-show-stats-response.json
:language: javascript
Configure Amphora
=================
.. rest_method:: PUT /v2/octavia/amphorae/{amphora_id}/config
Update the amphora agent configuration. This will push the new configuration
to the amphora agent and will update the configuration options that are
mutatable.
If you are not an administrative user, the service returns the HTTP
``Forbidden (403)`` response code.
This operation does not require a request body.
**New in version 2.7**
.. rest_status_code:: success ../http-status.yaml
- 202
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 403
- 404
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- amphora_id: path-amphora-id
Curl Example
------------
.. literalinclude:: examples/amphora-config-curl
:language: bash
Response
--------
There is no body content for the response of a successful PUT request.
Failover Amphora
================
@@ -236,6 +282,7 @@ This operation does not require a request body.
- 400
- 401
- 403
- 404
- 500
Request

View File

@@ -0,0 +1 @@
curl -X PUT -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2/octavia/amphorae/6bd55cd3-802e-447e-a518-1e74e23bb106/config

View File

@@ -86,6 +86,9 @@ class RootController(rest.RestController):
self._add_a_version(versions, 'v2.5', 'v2', 'SUPPORTED',
'2019-01-21T00:00:00Z', host_url)
# Flavors
self._add_a_version(versions, 'v2.6', 'v2', 'CURRENT',
self._add_a_version(versions, 'v2.6', 'v2', 'SUPPORTED',
'2019-01-25T00:00:00Z', host_url)
# Amphora Config update
self._add_a_version(versions, 'v2.7', 'v2', 'CURRENT',
'2018-01-25T12:00:00Z', host_url)
return {'versions': versions}

View File

@@ -83,6 +83,8 @@ class AmphoraController(base.BaseController):
if amphora_id and remainder:
controller = remainder[0]
remainder = remainder[1:]
if controller == 'config':
return AmphoraUpdateController(amp_id=amphora_id), remainder
if controller == 'failover':
return FailoverController(amp_id=amphora_id), remainder
if controller == 'stats':
@@ -136,6 +138,47 @@ class FailoverController(base.BaseController):
provisioning_status=constants.ERROR)
class AmphoraUpdateController(base.BaseController):
RBAC_TYPE = constants.RBAC_AMPHORA
def __init__(self, amp_id):
super(AmphoraUpdateController, self).__init__()
topic = cfg.CONF.oslo_messaging.topic
self.transport = messaging.get_rpc_transport(cfg.CONF)
self.target = messaging.Target(
namespace=constants.RPC_NAMESPACE_CONTROLLER_AGENT,
topic=topic, version="1.0", fanout=False)
self.client = messaging.RPCClient(self.transport, target=self.target)
self.amp_id = amp_id
@wsme_pecan.wsexpose(None, wtypes.text, status_code=202)
def put(self):
"""Update amphora agent configuration"""
pcontext = pecan.request.context
context = pcontext.get('octavia_context')
db_amp = self._get_db_amp(context.session, self.amp_id,
show_deleted=False)
# Check to see if the amphora is a spare (not associated with an LB)
if db_amp.load_balancer:
self._auth_validate_action(
context, db_amp.load_balancer.project_id,
constants.RBAC_PUT_CONFIG)
else:
self._auth_validate_action(
context, context.project_id, constants.RBAC_PUT_CONFIG)
try:
LOG.info("Sending amphora agent update request for amphora %s to "
"the queue.", self.amp_id)
payload = {constants.AMPHORA_ID: db_amp.id}
self.client.cast({}, 'update_amphora_agent_config', **payload)
except Exception:
with excutils.save_and_reraise_exception(reraise=True):
LOG.error("Unable to send amphora agent update request for "
"amphora %s to the queue.", self.amp_id)
class AmphoraStatsController(base.BaseController):
RBAC_TYPE = constants.RBAC_AMPHORA

View File

@@ -297,6 +297,7 @@ UPDATE_POOL_FLOW = 'octavia-update-pool-flow'
UPDATE_L7POLICY_FLOW = 'octavia-update-l7policy-flow'
UPDATE_L7RULE_FLOW = 'octavia-update-l7rule-flow'
UPDATE_AMPS_SUBFLOW = 'octavia-update-amps-subflow'
UPDATE_AMPHORA_CONFIG_FLOW = 'octavia-update-amp-config-flow'
POST_MAP_AMP_TO_LB_SUBFLOW = 'octavia-post-map-amp-to-lb-subflow'
CREATE_AMP_FOR_LB_SUBFLOW = 'octavia-create-amp-for-lb-subflow'
@@ -540,6 +541,7 @@ RBAC_FLAVOR = '{}:flavor:'.format(LOADBALANCER_API)
RBAC_FLAVOR_PROFILE = '{}:flavor-profile:'.format(LOADBALANCER_API)
RBAC_POST = 'post'
RBAC_PUT = 'put'
RBAC_PUT_CONFIG = 'put_config'
RBAC_PUT_FAILOVER = 'put_failover'
RBAC_DELETE = 'delete'
RBAC_GET_ONE = 'get_one'

View File

@@ -148,3 +148,8 @@ class Endpoint(object):
def delete_l7rule(self, context, l7rule_id):
LOG.info('Deleting l7rule \'%s\'...', l7rule_id)
self.worker.delete_l7rule(l7rule_id)
def update_amphora_agent_config(self, context, amphora_id):
LOG.info('Updating amphora \'%s\' agent configuration...',
amphora_id)
self.worker.update_amphora_agent_config(amphora_id)

View File

@@ -958,3 +958,32 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine):
with tf_logging.DynamicLoggingListener(certrotation_amphora_tf,
log=LOG):
certrotation_amphora_tf.run()
def update_amphora_agent_config(self, amphora_id):
"""Update the amphora agent configuration.
Note: This will update the amphora agent configuration file and
update the running configuration for mutatable configuration
items.
:param amphora_id: ID of the amphora to update.
:returns: None
"""
LOG.info("Start amphora agent configuration update, amphora's id "
"is: %s", amphora_id)
amp = self._amphora_repo.get(db_apis.get_session(), id=amphora_id)
lb = self._amphora_repo.get_lb_for_amphora(db_apis.get_session(),
amphora_id)
flavor = {}
if lb.flavor_id:
flavor = self._flavor_repo.get_flavor_metadata_dict(
db_apis.get_session(), lb.flavor_id)
update_amphora_tf = self._taskflow_load(
self._amphora_flows.update_amphora_config_flow(),
store={constants.AMPHORA: amp,
constants.FLAVOR: flavor})
with tf_logging.DynamicLoggingListener(update_amphora_tf,
log=LOG):
update_amphora_tf.run()

View File

@@ -533,3 +533,19 @@ class AmphoraFlows(object):
requires=constants.AMPHORA))
return rotated_amphora_flow
def update_amphora_config_flow(self):
"""Creates a flow to update the amphora agent configuration.
:returns: The flow for updating an amphora
"""
update_amphora_flow = linear_flow.Flow(
constants.UPDATE_AMPHORA_CONFIG_FLOW)
update_amphora_flow.add(lifecycle_tasks.AmphoraToErrorOnRevertTask(
requires=constants.AMPHORA))
update_amphora_flow.add(amphora_driver_tasks.AmphoraConfigUpdate(
requires=(constants.AMPHORA, constants.FLAVOR)))
return update_amphora_flow

View File

@@ -30,6 +30,14 @@ rules = [
"Show Amphora details",
[{'method': 'GET', 'path': '/v2/octavia/amphorae/{amphora_id}'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_AMPHORA,
action=constants.RBAC_PUT_CONFIG),
constants.RULE_API_ADMIN,
"Update Amphora Agent Configuration",
[{'method': 'PUT',
'path': '/v2/octavia/amphorae/{amphora_id}/config'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_AMPHORA,
action=constants.RBAC_PUT_FAILOVER),

View File

@@ -46,7 +46,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
versions = self._get_versions_with_config(
api_v1_enabled=True, api_v2_enabled=True)
version_ids = tuple(v.get('id') for v in versions)
self.assertEqual(8, len(version_ids))
self.assertEqual(9, len(version_ids))
self.assertIn('v1', version_ids)
self.assertIn('v2.0', version_ids)
self.assertIn('v2.1', version_ids)
@@ -55,6 +55,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
self.assertIn('v2.4', version_ids)
self.assertIn('v2.5', version_ids)
self.assertIn('v2.6', version_ids)
self.assertIn('v2.7', version_ids)
# Each version should have a 'self' 'href' to the API version URL
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]
@@ -74,7 +75,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
def test_api_v1_disabled(self):
versions = self._get_versions_with_config(
api_v1_enabled=False, api_v2_enabled=True)
self.assertEqual(7, len(versions))
self.assertEqual(8, len(versions))
self.assertEqual('v2.0', versions[0].get('id'))
self.assertEqual('v2.1', versions[1].get('id'))
self.assertEqual('v2.2', versions[2].get('id'))
@@ -82,6 +83,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
self.assertEqual('v2.4', versions[4].get('id'))
self.assertEqual('v2.5', versions[5].get('id'))
self.assertEqual('v2.6', versions[6].get('id'))
self.assertEqual('v2.7', versions[7].get('id'))
def test_api_v2_disabled(self):
versions = self._get_versions_with_config(

View File

@@ -77,6 +77,7 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
AMPHORA_PATH = AMPHORAE_PATH + '/{amphora_id}'
AMPHORA_FAILOVER_PATH = AMPHORA_PATH + '/failover'
AMPHORA_STATS_PATH = AMPHORA_PATH + '/stats'
AMPHORA_CONFIG_PATH = AMPHORA_PATH + '/config'
PROVIDERS_PATH = '/lbaas/providers'
FLAVOR_CAPABILITIES_PATH = (PROVIDERS_PATH +

View File

@@ -21,6 +21,7 @@ from oslo_utils import uuidutils
from octavia.common import constants
import octavia.common.context
from octavia.common import exceptions
from octavia.tests.functional.api.v2 import base
@@ -501,3 +502,101 @@ class TestAmphora(base.BaseAPITest):
self.amp2_id = self.amp2.id
self.get(self.AMPHORA_STATS_PATH.format(
amphora_id=self.amp2_id), status=404)
@mock.patch('oslo_messaging.RPCClient.cast')
def test_config(self, mock_cast):
self.put(self.AMPHORA_CONFIG_PATH.format(
amphora_id=self.amp_id), body={}, status=202)
payload = {constants.AMPHORA_ID: self.amp_id}
mock_cast.assert_called_with({}, 'update_amphora_agent_config',
**payload)
@mock.patch('oslo_messaging.RPCClient.cast')
def test_config_deleted(self, mock_cast):
new_amp = self._create_additional_amp()
self.amphora_repo.update(self.session, new_amp.id,
status=constants.DELETED)
self.put(self.AMPHORA_CONFIG_PATH.format(
amphora_id=new_amp.id), body={}, status=404)
self.assertFalse(mock_cast.called)
@mock.patch('oslo_messaging.RPCClient.cast')
def test_config_bad_amp_id(self, mock_cast):
self.put(self.AMPHORA_CONFIG_PATH.format(
amphora_id='bogus'), body={}, status=404)
self.assertFalse(mock_cast.called)
@mock.patch('oslo_messaging.RPCClient.cast')
def test_config_exception(self, mock_cast):
mock_cast.side_effect = exceptions.OctaviaException('boom')
self.put(self.AMPHORA_CONFIG_PATH.format(
amphora_id=self.amp_id), body={}, status=500)
@mock.patch('oslo_messaging.RPCClient.cast')
def test_config_spare_amp(self, mock_cast):
amp_args = {
'compute_id': uuidutils.generate_uuid(),
'status': constants.AMPHORA_READY,
'lb_network_ip': '192.168.1.2',
'cert_expiration': datetime.datetime.now(),
'cert_busy': False,
'cached_zone': 'zone1',
'created_at': datetime.datetime.now(),
'updated_at': datetime.datetime.now(),
'image_id': uuidutils.generate_uuid(),
}
amp = self.amphora_repo.create(self.session, **amp_args)
self.put(self.AMPHORA_CONFIG_PATH.format(
amphora_id=amp.id), body={}, status=202)
payload = {constants.AMPHORA_ID: amp.id}
mock_cast.assert_called_with({}, 'update_amphora_agent_config',
**payload)
@mock.patch('oslo_messaging.RPCClient.cast')
def test_config_authorized(self, mock_cast):
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
with mock.patch.object(octavia.common.context.Context, 'project_id',
self.project_id):
override_credentials = {
'service_user_id': None,
'user_domain_id': None,
'is_admin_project': True,
'service_project_domain_id': None,
'service_project_id': None,
'roles': ['load-balancer_member'],
'user_id': None,
'is_admin': True,
'service_user_domain_id': None,
'project_domain_id': None,
'service_roles': [],
'project_id': self.project_id}
with mock.patch(
"oslo_context.context.RequestContext.to_policy_values",
return_value=override_credentials):
self.put(self.AMPHORA_CONFIG_PATH.format(
amphora_id=self.amp_id), body={}, status=202)
# Reset api auth setting
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
payload = {constants.AMPHORA_ID: self.amp_id}
mock_cast.assert_called_with({}, 'update_amphora_agent_config',
**payload)
@mock.patch('oslo_messaging.RPCClient.cast')
def test_config_not_authorized(self, mock_cast):
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
with mock.patch.object(octavia.common.context.Context, 'project_id',
uuidutils.generate_uuid()):
self.put(self.AMPHORA_CONFIG_PATH.format(
amphora_id=self.amp_id), body={}, status=403)
# Reset api auth setting
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertFalse(mock_cast.called)
def test_bogus_path(self):
self.put(self.AMPHORA_PATH.format(amphora_id=self.amp_id) + '/bogus',
body={}, status=405)

View File

@@ -175,3 +175,8 @@ class TestEndpoint(base.TestCase):
self.ep.delete_l7rule(self.context, self.resource_id)
self.ep.worker.delete_l7rule.assert_called_once_with(
self.resource_id)
def test_update_amphora_agent_config(self):
self.ep.update_amphora_agent_config(self.context, self.resource_id)
self.ep.worker.update_amphora_agent_config.assert_called_once_with(
self.resource_id)

View File

@@ -408,3 +408,15 @@ class TestAmphoraFlows(base.TestCase):
self.assertEqual(1, len(amp_flow.provides))
self.assertEqual(2, len(amp_flow.requires))
def test_update_amphora_config_flow(self, mock_get_net_driver):
amp_flow = self.AmpFlow.update_amphora_config_flow()
self.assertIsInstance(amp_flow, flow.Flow)
self.assertIn(constants.AMPHORA, amp_flow.requires)
self.assertIn(constants.FLAVOR, amp_flow.requires)
self.assertEqual(2, len(amp_flow.requires))
self.assertEqual(0, len(amp_flow.provides))

View File

@@ -1408,3 +1408,58 @@ class TestControllerWorker(base.TestCase):
constants.AMPHORA_ID:
_amphora_mock.id}))
_flow_mock.run.assert_called_once_with()
@mock.patch('octavia.db.repositories.FlavorRepository.'
'get_flavor_metadata_dict')
@mock.patch('octavia.db.repositories.AmphoraRepository.get_lb_for_amphora')
@mock.patch('octavia.controller.worker.flows.'
'amphora_flows.AmphoraFlows.update_amphora_config_flow',
return_value=_flow_mock)
def test_update_amphora_agent_config(self,
mock_update_flow,
mock_get_lb_for_amp,
mock_flavor_meta,
mock_api_get_session,
mock_dyn_log_listener,
mock_taskflow_load,
mock_pool_repo_get,
mock_member_repo_get,
mock_l7rule_repo_get,
mock_l7policy_repo_get,
mock_listener_repo_get,
mock_lb_repo_get,
mock_health_mon_repo_get,
mock_amp_repo_get):
_flow_mock.reset_mock()
mock_lb = mock.MagicMock()
mock_lb.flavor_id = 'vanilla'
mock_get_lb_for_amp.return_value = mock_lb
mock_flavor_meta.return_value = {'test': 'dict'}
cw = controller_worker.ControllerWorker()
cw.update_amphora_agent_config(AMP_ID)
mock_amp_repo_get.assert_called_once_with(_db_session, id=AMP_ID)
mock_get_lb_for_amp.assert_called_once_with(_db_session, AMP_ID)
mock_flavor_meta.assert_called_once_with(_db_session, 'vanilla')
(base_taskflow.BaseTaskFlowEngine._taskflow_load.
assert_called_once_with(_flow_mock,
store={constants.AMPHORA: _amphora_mock,
constants.FLAVOR: {'test': 'dict'}}))
_flow_mock.run.assert_called_once_with()
# Test with no flavor
_flow_mock.reset_mock()
mock_amp_repo_get.reset_mock()
mock_get_lb_for_amp.reset_mock()
mock_flavor_meta.reset_mock()
base_taskflow.BaseTaskFlowEngine._taskflow_load.reset_mock()
mock_lb.flavor_id = None
cw.update_amphora_agent_config(AMP_ID)
mock_amp_repo_get.assert_called_once_with(_db_session, id=AMP_ID)
mock_get_lb_for_amp.assert_called_once_with(_db_session, AMP_ID)
mock_flavor_meta.assert_not_called()
(base_taskflow.BaseTaskFlowEngine._taskflow_load.
assert_called_once_with(_flow_mock,
store={constants.AMPHORA: _amphora_mock,
constants.FLAVOR: {}}))
_flow_mock.run.assert_called_once_with()

View File

@@ -0,0 +1,12 @@
---
features:
- |
Octavia now has an administrative API that updates the amphora agent
configuration on running amphora.
upgrade:
- |
When the amphora agent configuration update API is called on an amphora
running a version of the amphora agent that does not support configuration
updates, an ERROR log message will be posted to the controller log file
indicating that the amphora does not support agent configuration updates.
In this case, the amphora image should be updated to a newer version.