From 52ffdd16a6d734361a3d6f71b1869809d36678cc Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Wed, 23 Jan 2019 17:26:22 -0800 Subject: [PATCH] Add amphora agent configuration update admin API This patch adds a new admin API that updates an amphora's agent configuration. Change-Id: I41ce6843fb53fa21ab84e5b1d0734e70380d716a --- api-ref/source/v2/amphora.inc | 47 +++++++++ .../source/v2/examples/amphora-config-curl | 1 + octavia/api/root_controller.py | 5 +- octavia/api/v2/controllers/amphora.py | 43 ++++++++ octavia/common/constants.py | 2 + octavia/controller/queue/endpoint.py | 5 + .../controller/worker/controller_worker.py | 29 ++++++ .../controller/worker/flows/amphora_flows.py | 16 +++ octavia/policies/amphora.py | 8 ++ .../functional/api/test_root_controller.py | 6 +- octavia/tests/functional/api/v2/base.py | 1 + .../tests/functional/api/v2/test_amphora.py | 99 +++++++++++++++++++ .../unit/controller/queue/test_endpoint.py | 5 + .../worker/flows/test_amphora_flows.py | 12 +++ .../worker/test_controller_worker.py | 55 +++++++++++ ...nt-config-update-API-298b31e6c0cd715c.yaml | 12 +++ 16 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 api-ref/source/v2/examples/amphora-config-curl create mode 100644 releasenotes/notes/Add-amphora-agent-config-update-API-298b31e6c0cd715c.yaml diff --git a/api-ref/source/v2/amphora.inc b/api-ref/source/v2/amphora.inc index e150e42203..ef4b20106d 100644 --- a/api-ref/source/v2/amphora.inc +++ b/api-ref/source/v2/amphora.inc @@ -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 diff --git a/api-ref/source/v2/examples/amphora-config-curl b/api-ref/source/v2/examples/amphora-config-curl new file mode 100644 index 0000000000..a0fc0a921f --- /dev/null +++ b/api-ref/source/v2/examples/amphora-config-curl @@ -0,0 +1 @@ +curl -X PUT -H "X-Auth-Token: " http://198.51.100.10:9876/v2/octavia/amphorae/6bd55cd3-802e-447e-a518-1e74e23bb106/config diff --git a/octavia/api/root_controller.py b/octavia/api/root_controller.py index e9fcce12da..7ee3f8064e 100644 --- a/octavia/api/root_controller.py +++ b/octavia/api/root_controller.py @@ -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} diff --git a/octavia/api/v2/controllers/amphora.py b/octavia/api/v2/controllers/amphora.py index ff2846a4e7..0a2244b264 100644 --- a/octavia/api/v2/controllers/amphora.py +++ b/octavia/api/v2/controllers/amphora.py @@ -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 diff --git a/octavia/common/constants.py b/octavia/common/constants.py index 05e5393cf1..8a685fba2e 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -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' diff --git a/octavia/controller/queue/endpoint.py b/octavia/controller/queue/endpoint.py index 82a7077105..72af65fbe6 100644 --- a/octavia/controller/queue/endpoint.py +++ b/octavia/controller/queue/endpoint.py @@ -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) diff --git a/octavia/controller/worker/controller_worker.py b/octavia/controller/worker/controller_worker.py index c9cda0a1dc..6b7d109ba7 100644 --- a/octavia/controller/worker/controller_worker.py +++ b/octavia/controller/worker/controller_worker.py @@ -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() diff --git a/octavia/controller/worker/flows/amphora_flows.py b/octavia/controller/worker/flows/amphora_flows.py index a5f45b1909..c7a222bbfd 100644 --- a/octavia/controller/worker/flows/amphora_flows.py +++ b/octavia/controller/worker/flows/amphora_flows.py @@ -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 diff --git a/octavia/policies/amphora.py b/octavia/policies/amphora.py index bcc85df23d..a4d4e87955 100644 --- a/octavia/policies/amphora.py +++ b/octavia/policies/amphora.py @@ -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), diff --git a/octavia/tests/functional/api/test_root_controller.py b/octavia/tests/functional/api/test_root_controller.py index 85f7ca1c03..b3c654f79a 100644 --- a/octavia/tests/functional/api/test_root_controller.py +++ b/octavia/tests/functional/api/test_root_controller.py @@ -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( diff --git a/octavia/tests/functional/api/v2/base.py b/octavia/tests/functional/api/v2/base.py index c229c98f1c..199a69706a 100644 --- a/octavia/tests/functional/api/v2/base.py +++ b/octavia/tests/functional/api/v2/base.py @@ -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 + diff --git a/octavia/tests/functional/api/v2/test_amphora.py b/octavia/tests/functional/api/v2/test_amphora.py index 841a84a1b5..0d6ffe2348 100644 --- a/octavia/tests/functional/api/v2/test_amphora.py +++ b/octavia/tests/functional/api/v2/test_amphora.py @@ -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) diff --git a/octavia/tests/unit/controller/queue/test_endpoint.py b/octavia/tests/unit/controller/queue/test_endpoint.py index f72f6b19ce..9557bfc06b 100644 --- a/octavia/tests/unit/controller/queue/test_endpoint.py +++ b/octavia/tests/unit/controller/queue/test_endpoint.py @@ -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) diff --git a/octavia/tests/unit/controller/worker/flows/test_amphora_flows.py b/octavia/tests/unit/controller/worker/flows/test_amphora_flows.py index 943b629a54..18a316700e 100644 --- a/octavia/tests/unit/controller/worker/flows/test_amphora_flows.py +++ b/octavia/tests/unit/controller/worker/flows/test_amphora_flows.py @@ -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)) diff --git a/octavia/tests/unit/controller/worker/test_controller_worker.py b/octavia/tests/unit/controller/worker/test_controller_worker.py index d6d7854736..46d67e660e 100644 --- a/octavia/tests/unit/controller/worker/test_controller_worker.py +++ b/octavia/tests/unit/controller/worker/test_controller_worker.py @@ -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() diff --git a/releasenotes/notes/Add-amphora-agent-config-update-API-298b31e6c0cd715c.yaml b/releasenotes/notes/Add-amphora-agent-config-update-API-298b31e6c0cd715c.yaml new file mode 100644 index 0000000000..b3959b183b --- /dev/null +++ b/releasenotes/notes/Add-amphora-agent-config-update-API-298b31e6c0cd715c.yaml @@ -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.