diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index ceecfcfd3..81856454a 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -128,3 +128,14 @@ Flavor Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_flavor .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_flavor .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_flavor + +Amphora Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.amphorae + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_amphora + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_amphora + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.configure_amphora + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.failover_amphora diff --git a/doc/source/user/resources/load_balancer/index.rst b/doc/source/user/resources/load_balancer/index.rst index dbb8226f2..171146ff8 100644 --- a/doc/source/user/resources/load_balancer/index.rst +++ b/doc/source/user/resources/load_balancer/index.rst @@ -14,3 +14,4 @@ Load Balancer Resources v2/provider v2/flavor_profile v2/flavor + v2/amphora diff --git a/doc/source/user/resources/load_balancer/v2/amphora.rst b/doc/source/user/resources/load_balancer/v2/amphora.rst new file mode 100644 index 000000000..c89d1ee86 --- /dev/null +++ b/doc/source/user/resources/load_balancer/v2/amphora.rst @@ -0,0 +1,30 @@ +openstack.load_balancer.v2.amphora +================================== + +.. automodule:: openstack.load_balancer.v2.amphora + +The Amphora Class +----------------- + +The ``Amphora`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.amphora.Amphora + :members: + +The AmphoraConfig Class +----------------------- + +The ``AmphoraConfig`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.amphora.AmphoraConfig + :members: + +The AmphoraFailover Class +------------------------- + +The ``AmphoraFailover`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.amphora.AmphoraFailover + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index edf42be30..6b1f8c7e2 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer.v2 import amphora as _amphora from openstack.load_balancer.v2 import flavor as _flavor from openstack.load_balancer.v2 import flavor_profile as _flavor_profile from openstack.load_balancer.v2 import health_monitor as _hm @@ -953,3 +954,54 @@ class Proxy(proxy.Proxy): :rtype: :class:`~openstack.load_balancer.v2.flavor.Flavor` """ return self._update(_flavor.Flavor, flavor, **attrs) + + def amphorae(self, **query): + """Retrieve a generator of amphorae + + :returns: A generator of amphora instances + """ + return self._list(_amphora.Amphora, **query) + + def get_amphora(self, *attrs): + """Get a amphora + + :param amphora: The value can be the ID of an amphora + or :class:`~openstack.load_balancer.v2.amphora.Amphora` instance. + + :returns: One + :class:`~openstack.load_balancer.v2.amphora.Amphora` + """ + return self._get(_amphora.Amphora, *attrs) + + def find_amphora(self, amphora_id, ignore_missing=True): + """Find a single amphora + + :param amphora_id: The ID of a amphora + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the amphora does not exist. + When set to ``True``, no exception will be set when attempting + to find a nonexistent amphora. + + :returns: ``None`` + """ + return self._find(_amphora.Amphora, amphora_id, + ignore_missing=ignore_missing) + + def configure_amphora(self, amphora_id, **attrs): + """Update the configuration of an amphora agent + + :param amphora_id: The ID of an amphora + + :returns: ``None`` + """ + return self._update(_amphora.AmphoraConfig, amphora_id=amphora_id) + + def failover_amphora(self, amphora_id, **attrs): + """Failover an amphora + + :param amphora_id: The ID of an amphora + + :returns: ``None`` + """ + return self._update(_amphora.AmphoraFailover, amphora_id=amphora_id) diff --git a/openstack/load_balancer/v2/amphora.py b/openstack/load_balancer/v2/amphora.py new file mode 100644 index 000000000..bedae8333 --- /dev/null +++ b/openstack/load_balancer/v2/amphora.py @@ -0,0 +1,148 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack import resource + + +class Amphora(resource.Resource): + resource_key = 'amphora' + resources_key = 'amphorae' + base_path = '/octavia/amphorae' + + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = True + + _query_mapping = resource.QueryParameters( + 'id', 'loadbalancer_id', 'compute_id', 'lb_network_ip', 'vrrp_ip', + 'ha_ip', 'vrrp_port_id', 'ha_port_id', 'cert_expiration', 'cert_busy', + 'role', 'status', 'vrrp_interface', 'vrrp_id', 'vrrp_priority', + 'cached_zone', 'created_at', 'updated_at', 'image_id', 'image_id' + ) + + # Properties + #: The ID of the amphora. + id = resource.Body('id') + #: The ID of the load balancer. + loadbalancer_id = resource.Body('loadbalancer_id') + #: The ID of the amphora resource in the compute system. + compute_id = resource.Body('compute_id') + #: The management IP of the amphora. + lb_network_ip = resource.Body('lb_network_ip') + #: The address of the vrrp port on the amphora. + vrrp_ip = resource.Body('vrrp_ip') + #: The IP address of the Virtual IP (VIP). + ha_ip = resource.Body('ha_ip') + #: The vrrp port's ID in the networking system. + vrrp_port_id = resource.Body('vrrp_port_id') + #: The ID of the Virtual IP (VIP) port. + ha_port_id = resource.Body('ha_port_id') + #: The date the certificate for the amphora expires. + cert_expiration = resource.Body('cert_expiration') + #: Whether the certificate is in the process of being replaced. + cert_busy = resource.Body('cert_busy') + #: The role configured for the amphora. One of STANDALONE, MASTER, BACKUP. + role = resource.Body('role') + #: The status of the amphora. One of: BOOTING, ALLOCATED, READY, + #: PENDING_CREATE, PENDING_DELETE, DELETED, ERROR. + status = resource.Body('status') + #: The bound interface name of the vrrp port on the amphora. + vrrp_interface = resource.Body('vrrp_interface') + #: The vrrp group's ID for the amphora. + vrrp_id = resource.Body('vrrp_id') + #: The priority of the amphora in the vrrp group. + vrrp_priority = resource.Body('vrrp_priority') + #: The availability zone of a compute instance, cached at create time. + cached_zone = resource.Body('cached_zone') + #: The UTC date and timestamp when the resource was created. + created_at = resource.Body('created_at') + #: The UTC date and timestamp when the resource was last updated. + updated_at = resource.Body('updated_at') + #: The ID of the glance image used for the amphora. + image_id = resource.Body('image_id') + #: The ID of the compute flavor used for the amphora. + compute_flavor = resource.Body('compute_flavor') + + +class AmphoraConfig(resource.Resource): + base_path = '/octavia/amphorae/%(amphora_id)s/config' + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = True + allow_delete = False + allow_list = False + + requires_id = False + + # Properties + #: The ID of the amphora. + amphora_id = resource.URI('amphora_id') + + # The parent commit method assumes there is a header or body change, + # which we do not have here. The default _update code path also has no + # way to pass has_body into this function, so overriding the method here. + def commit(self, session, base_path=None): + kwargs = {} + request = self._prepare_request(prepend_key=False, + base_path=base_path, + **kwargs) + session = self._get_session(session) + kwargs = {} + microversion = self._get_microversion_for(session, 'commit') + response = session.put(request.url, json=request.body, + headers=request.headers, + microversion=microversion, **kwargs) + self.microversion = microversion + self._translate_response(response, has_body=False) + return self + + +class AmphoraFailover(resource.Resource): + base_path = '/octavia/amphorae/%(amphora_id)s/failover' + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = True + allow_delete = False + allow_list = False + + requires_id = False + + # Properties + #: The ID of the amphora. + amphora_id = resource.URI('amphora_id') + + # The parent commit method assumes there is a header or body change, + # which we do not have here. The default _update code path also has no + # way to pass has_body into this function, so overriding the method here. + def commit(self, session, base_path=None): + kwargs = {} + request = self._prepare_request(prepend_key=False, + base_path=base_path, + **kwargs) + session = self._get_session(session) + kwargs = {} + microversion = self._get_microversion_for(session, 'commit') + response = session.put(request.url, json=request.body, + headers=request.headers, + microversion=microversion, **kwargs) + self.microversion = microversion + self._translate_response(response, has_body=False) + return self diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 74d55b30a..a9b984653 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -37,6 +37,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): PROJECT_ID = None FLAVOR_PROFILE_ID = None FLAVOR_ID = None + AMPHORA_ID = None PROTOCOL = 'HTTP' PROTOCOL_PORT = 80 LB_ALGORITHM = 'ROUND_ROBIN' @@ -117,6 +118,10 @@ class TestLoadBalancer(base.BaseFunctionalTest): wait=self._wait_for_timeout) self.LB_ID = test_lb.id + amphorae = self.conn.load_balancer.amphorae(loadbalancer_id=self.LB_ID) + for amp in amphorae: + self.AMPHORA_ID = amp.id + test_listener = self.conn.load_balancer.create_listener( name=self.LISTENER_NAME, protocol=self.PROTOCOL, protocol_port=self.PROTOCOL_PORT, loadbalancer_id=self.LB_ID) @@ -573,3 +578,25 @@ class TestLoadBalancer(base.BaseFunctionalTest): self.FLAVOR_ID, name=self.FLAVOR_NAME) test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) self.assertEqual(self.FLAVOR_NAME, test_flavor.name) + + def test_amphora_list(self): + amp_ids = [amp.id for amp in self.conn.load_balancer.amphorae()] + self.assertIn(self.AMPHORA_ID, amp_ids) + + def test_amphora_find(self): + test_amphora = self.conn.load_balancer.find_amphora(self.AMPHORA_ID) + self.assertEqual(self.AMPHORA_ID, test_amphora.id) + + def test_amphora_get(self): + test_amphora = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) + self.assertEqual(self.AMPHORA_ID, test_amphora.id) + + def test_amphora_configure(self): + self.conn.load_balancer.configure_amphora(self.AMPHORA_ID) + test_amp = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) + self.assertEqual(self.AMPHORA_ID, test_amp.id) + + def test_amphora_failover(self): + self.conn.load_balancer.failover_amphora(self.AMPHORA_ID) + test_amp = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) + self.assertEqual(self.AMPHORA_ID, test_amp.id) diff --git a/openstack/tests/unit/load_balancer/test_amphora.py b/openstack/tests/unit/load_balancer/test_amphora.py new file mode 100644 index 000000000..03d44f81d --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_amphora.py @@ -0,0 +1,142 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack.tests.unit import base +import uuid + +from openstack.load_balancer.v2 import amphora + +IDENTIFIER = uuid.uuid4() +LB_ID = uuid.uuid4() +LISTENER_ID = uuid.uuid4() +COMPUTE_ID = uuid.uuid4() +VRRP_PORT_ID = uuid.uuid4() +HA_PORT_ID = uuid.uuid4() +IMAGE_ID = uuid.uuid4() +COMPUTE_FLAVOR = uuid.uuid4() +AMPHORA_ID = uuid.uuid4() + +EXAMPLE = { + 'id': IDENTIFIER, + 'loadbalancer_id': LB_ID, + 'compute_id': COMPUTE_ID, + 'lb_network_ip': '192.168.1.2', + 'vrrp_ip': '192.168.1.5', + 'ha_ip': '192.168.1.10', + 'vrrp_port_id': VRRP_PORT_ID, + 'ha_port_id': HA_PORT_ID, + 'cert_expiration': '2019-09-19 00:34:51', + 'cert_busy': 0, + 'role': 'MASTER', + 'status': 'ALLOCATED', + 'vrrp_interface': 'eth1', + 'vrrp_id': 1, + 'vrrp_priority': 100, + 'cached_zone': 'zone1', + 'created_at': '2017-05-10T18:14:44', + 'updated_at': '2017-05-10T23:08:12', + 'image_id': IMAGE_ID, + 'compute_flavor': COMPUTE_FLAVOR +} + + +class TestAmphora(base.TestCase): + + def test_basic(self): + test_amphora = amphora.Amphora() + self.assertEqual('amphora', test_amphora.resource_key) + self.assertEqual('amphorae', test_amphora.resources_key) + self.assertEqual('/octavia/amphorae', test_amphora.base_path) + self.assertFalse(test_amphora.allow_create) + self.assertTrue(test_amphora.allow_fetch) + self.assertFalse(test_amphora.allow_commit) + self.assertFalse(test_amphora.allow_delete) + self.assertTrue(test_amphora.allow_list) + + def test_make_it(self): + test_amphora = amphora.Amphora(**EXAMPLE) + self.assertEqual(IDENTIFIER, test_amphora.id) + self.assertEqual(LB_ID, test_amphora.loadbalancer_id) + self.assertEqual(COMPUTE_ID, test_amphora.compute_id) + self.assertEqual(EXAMPLE['lb_network_ip'], test_amphora.lb_network_ip) + self.assertEqual(EXAMPLE['vrrp_ip'], test_amphora.vrrp_ip) + self.assertEqual(EXAMPLE['ha_ip'], test_amphora.ha_ip) + self.assertEqual(VRRP_PORT_ID, test_amphora.vrrp_port_id) + self.assertEqual(HA_PORT_ID, test_amphora.ha_port_id) + self.assertEqual(EXAMPLE['cert_expiration'], + test_amphora.cert_expiration) + self.assertEqual(EXAMPLE['cert_busy'], test_amphora.cert_busy) + self.assertEqual(EXAMPLE['role'], test_amphora.role) + self.assertEqual(EXAMPLE['status'], test_amphora.status) + self.assertEqual(EXAMPLE['vrrp_interface'], + test_amphora.vrrp_interface) + self.assertEqual(EXAMPLE['vrrp_id'], test_amphora.vrrp_id) + self.assertEqual(EXAMPLE['vrrp_priority'], test_amphora.vrrp_priority) + self.assertEqual(EXAMPLE['cached_zone'], test_amphora.cached_zone) + self.assertEqual(EXAMPLE['created_at'], test_amphora.created_at) + self.assertEqual(EXAMPLE['updated_at'], test_amphora.updated_at) + self.assertEqual(IMAGE_ID, test_amphora.image_id) + self.assertEqual(COMPUTE_FLAVOR, test_amphora.compute_flavor) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'id': 'id', + 'loadbalancer_id': 'loadbalancer_id', + 'compute_id': 'compute_id', + 'lb_network_ip': 'lb_network_ip', + 'vrrp_ip': 'vrrp_ip', + 'ha_ip': 'ha_ip', + 'vrrp_port_id': 'vrrp_port_id', + 'ha_port_id': 'ha_port_id', + 'cert_expiration': 'cert_expiration', + 'cert_busy': 'cert_busy', + 'role': 'role', + 'status': 'status', + 'vrrp_interface': 'vrrp_interface', + 'vrrp_id': 'vrrp_id', + 'vrrp_priority': 'vrrp_priority', + 'cached_zone': 'cached_zone', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'image_id': 'image_id', + 'image_id': 'image_id' + }, + test_amphora._query_mapping._mapping) + + +class TestAmphoraConfig(base.TestCase): + + def test_basic(self): + test_amp_config = amphora.AmphoraConfig() + self.assertEqual('/octavia/amphorae/%(amphora_id)s/config', + test_amp_config.base_path) + self.assertFalse(test_amp_config.allow_create) + self.assertFalse(test_amp_config.allow_fetch) + self.assertTrue(test_amp_config.allow_commit) + self.assertFalse(test_amp_config.allow_delete) + self.assertFalse(test_amp_config.allow_list) + + +class TestAmphoraFailover(base.TestCase): + + def test_basic(self): + test_amp_failover = amphora.AmphoraFailover() + self.assertEqual('/octavia/amphorae/%(amphora_id)s/failover', + test_amp_failover.base_path) + self.assertFalse(test_amp_failover.allow_create) + self.assertFalse(test_amp_failover.allow_fetch) + self.assertTrue(test_amp_failover.allow_commit) + self.assertFalse(test_amp_failover.allow_delete) + self.assertFalse(test_amp_failover.allow_list) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index fafc9d10e..0f90b6a66 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -14,6 +14,7 @@ import uuid import mock from openstack.load_balancer.v2 import _proxy +from openstack.load_balancer.v2 import amphora from openstack.load_balancer.v2 import flavor from openstack.load_balancer.v2 import flavor_profile from openstack.load_balancer.v2 import health_monitor @@ -36,6 +37,7 @@ class TestLoadBalancerProxy(test_proxy_base.TestProxyBase): POOL_ID = uuid.uuid4() L7_POLICY_ID = uuid.uuid4() AMPHORA = 'amphora' + AMPHORA_ID = uuid.uuid4() def setUp(self): super(TestLoadBalancerProxy, self).setUp() @@ -363,3 +365,26 @@ class TestLoadBalancerProxy(test_proxy_base.TestProxyBase): def test_flavor_update(self): self.verify_update(self.proxy.update_flavor, flavor.Flavor) + + def test_amphorae(self): + self.verify_list(self.proxy.amphorae, amphora.Amphora) + + def test_amphora_get(self): + self.verify_get(self.proxy.get_amphora, amphora.Amphora) + + def test_amphora_find(self): + self.verify_find(self.proxy.find_amphora, amphora.Amphora) + + def test_amphora_configure(self): + self.verify_update(self.proxy.configure_amphora, + amphora.AmphoraConfig, + value=[self.AMPHORA_ID], + expected_args=[], + expected_kwargs={'amphora_id': self.AMPHORA_ID}) + + def test_amphora_failover(self): + self.verify_update(self.proxy.failover_amphora, + amphora.AmphoraFailover, + value=[self.AMPHORA_ID], + expected_args=[], + expected_kwargs={'amphora_id': self.AMPHORA_ID}) diff --git a/releasenotes/notes/add-octavia-amphora-api-7f3586f6a4f31de4.yaml b/releasenotes/notes/add-octavia-amphora-api-7f3586f6a4f31de4.yaml new file mode 100644 index 000000000..af9a5b409 --- /dev/null +++ b/releasenotes/notes/add-octavia-amphora-api-7f3586f6a4f31de4.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds Octavia (load_balancer) support for the amphora APIs.