From 4033c417904f44dc2f4fb05456791734b8b05bbc Mon Sep 17 00:00:00 2001 From: Sam Morrison Date: Mon, 18 Nov 2019 15:47:35 +1100 Subject: [PATCH] Add support for availability zone [profiles] Can now also pass the --availability-zone argument when creating a load balancer. Co-Authored-By: Adam Harwell Change-Id: I7280d2e8d027733e34be6a92b096f7e054563d62 --- doc/source/cli/index.rst | 14 + octaviaclient/api/constants.py | 16 +- octaviaclient/api/v2/octavia.py | 167 +++++++++++- octaviaclient/osc/v2/availabilityzone.py | 250 ++++++++++++++++++ .../osc/v2/availabilityzoneprofile.py | 188 +++++++++++++ octaviaclient/osc/v2/constants.py | 30 ++- octaviaclient/osc/v2/flavor.py | 2 +- octaviaclient/osc/v2/load_balancer.py | 12 + octaviaclient/osc/v2/provider.py | 44 ++- octaviaclient/osc/v2/utils.py | 54 +++- octaviaclient/tests/unit/api/test_octavia.py | 196 +++++++++++++- octaviaclient/tests/unit/osc/v2/constants.py | 18 +- .../unit/osc/v2/test_availabilityzone.py | 228 ++++++++++++++++ .../osc/v2/test_availabilityzoneprofile.py | 185 +++++++++++++ .../tests/unit/osc/v2/test_provider.py | 64 ++++- .../add-az-and-profiles-ed79c945c4e0d418.yaml | 9 + setup.cfg | 13 +- 17 files changed, 1463 insertions(+), 27 deletions(-) create mode 100644 octaviaclient/osc/v2/availabilityzone.py create mode 100644 octaviaclient/osc/v2/availabilityzoneprofile.py create mode 100644 octaviaclient/tests/unit/osc/v2/test_availabilityzone.py create mode 100644 octaviaclient/tests/unit/osc/v2/test_availabilityzoneprofile.py create mode 100644 releasenotes/notes/add-az-and-profiles-ed79c945c4e0d418.yaml diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 076e5dd..c3b6aea 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -111,3 +111,17 @@ flavorprofile .. autoprogram-cliff:: openstack.load_balancer.v2 :command: loadbalancer flavorprofile * + +================ +availabilityzone +================ + +.. autoprogram-cliff:: openstack.load_balancer.v2 + :command: loadbalancer availabilityzone * + +======================= +availabilityzoneprofile +======================= + +.. autoprogram-cliff:: openstack.load_balancer.v2 + :command: loadbalancer availabilityzoneprofile * diff --git a/octaviaclient/api/constants.py b/octaviaclient/api/constants.py index ab39171..dd9a706 100644 --- a/octaviaclient/api/constants.py +++ b/octaviaclient/api/constants.py @@ -48,11 +48,21 @@ BASE_AMPHORA_CONFIGURE_URL = BASE_SINGLE_AMPHORA_URL + '/config' BASE_AMPHORA_FAILOVER_URL = BASE_SINGLE_AMPHORA_URL + '/failover' BASE_PROVIDER_URL = BASE_LBAAS_ENDPOINT + "/providers" -BASE_PROVIDER_FLAVOR_CAPABILITY_URL = (BASE_LBAAS_ENDPOINT + - "/providers/{provider}/" - "flavor_capabilities") +BASE_PROVIDER_FLAVOR_CAPABILITY_URL = ( + BASE_LBAAS_ENDPOINT + "/providers/{provider}/flavor_capabilities") +BASE_PROVIDER_AVAILABILITY_ZONE_CAPABILITY_URL = ( + BASE_LBAAS_ENDPOINT + "/providers/{provider}" + "/availability_zone_capabilities" +) BASE_FLAVOR_URL = BASE_LBAAS_ENDPOINT + "/flavors" BASE_SINGLE_FLAVOR_URL = BASE_FLAVOR_URL + "/{uuid}" BASE_FLAVORPROFILE_URL = BASE_LBAAS_ENDPOINT + "/flavorprofiles" BASE_SINGLE_FLAVORPROFILE_URL = BASE_FLAVORPROFILE_URL + "/{uuid}" + +BASE_AVAILABILITYZONE_URL = BASE_LBAAS_ENDPOINT + "/availabilityzones" +BASE_SINGLE_AVAILABILITYZONE_URL = BASE_AVAILABILITYZONE_URL + "/{name}" +BASE_AVAILABILITYZONEPROFILE_URL = (BASE_LBAAS_ENDPOINT + + "/availabilityzoneprofiles") +BASE_SINGLE_AVAILABILITYZONEPROFILE_URL = (BASE_AVAILABILITYZONEPROFILE_URL + + "/{uuid}") diff --git a/octaviaclient/api/v2/octavia.py b/octaviaclient/api/v2/octavia.py index 1d883c5..2bb4515 100644 --- a/octaviaclient/api/v2/octavia.py +++ b/octaviaclient/api/v2/octavia.py @@ -784,13 +784,13 @@ class OctaviaAPI(api.BaseAPI): return response - def provider_capability_list(self, provider): - """Show the flavor capability of the specified provider. + def provider_flavor_capability_list(self, provider): + """Show the flavor capabilities of the specified provider. :param string provider: The name of the provider to show :return: - A ``dict`` containing the capabilicy of provider + A ``dict`` containing the capabilities of the provider """ url = const.BASE_PROVIDER_FLAVOR_CAPABILITY_URL.format( provider=provider) @@ -798,6 +798,20 @@ class OctaviaAPI(api.BaseAPI): return response + def provider_availability_zone_capability_list(self, provider): + """Show the availability zone capabilities of the specified provider. + + :param string provider: + The name of the provider to show + :return: + A ``dict`` containing the capabilities of the provider + """ + url = const.BASE_PROVIDER_AVAILABILITY_ZONE_CAPABILITY_URL.format( + provider=provider) + response = self._list(url) + + return response + def flavor_list(self, **kwargs): """List all flavors @@ -937,3 +951,150 @@ class OctaviaAPI(api.BaseAPI): response = self._delete(url) return response + + def availabilityzone_list(self, **kwargs): + """List all availabilityzones + + :param kwargs: + Parameters to filter on + :return: + A ``dict`` containing a list of availabilityzone + """ + url = const.BASE_AVAILABILITYZONE_URL + response = self._list(path=url, **kwargs) + + return response + + @correct_return_codes + def availabilityzone_delete(self, availabilityzone_name): + """Delete a availabilityzone + + :param string availabilityzone_name: + Name of the availabilityzone to delete + :return: + Response Code from the API + """ + url = const.BASE_SINGLE_AVAILABILITYZONE_URL.format( + name=availabilityzone_name) + response = self._delete(url) + + return response + + @correct_return_codes + def availabilityzone_create(self, **kwargs): + """Create a availabilityzone + + :param kwargs: + Parameters to create a availabilityzone with (expects json=) + :return: + A dict of the created availabilityzone's settings + """ + url = const.BASE_AVAILABILITYZONE_URL + response = self._create(url, **kwargs) + + return response + + @correct_return_codes + def availabilityzone_set(self, availabilityzone_name, **kwargs): + """Update a availabilityzone's settings + + :param string availabilityzone_name: + Name of the availabilityzone to update + :param kwargs: + A dict of arguments to update a availabilityzone + :return: + Response Code from the API + """ + url = const.BASE_SINGLE_AVAILABILITYZONE_URL.format( + name=availabilityzone_name) + response = self._create(url, method='PUT', **kwargs) + + return response + + @correct_return_codes + def availabilityzone_show(self, availabilityzone_name): + """Show a availabilityzone + + :param string availabilityzone_name: + Name of the availabilityzone to show + :return: + A dict of the specified availabilityzone's settings + """ + response = self._find(path=const.BASE_AVAILABILITYZONE_URL, + value=availabilityzone_name) + + return response + + @correct_return_codes + def availabilityzoneprofile_create(self, **kwargs): + """Create a availabilityzone profile + + :param kwargs: + Parameters to create a availabilityzone profile with + (expects json=) + :return: + A dict of the created availabilityzone profile's settings + """ + url = const.BASE_AVAILABILITYZONEPROFILE_URL + response = self._create(url, **kwargs) + + return response + + def availabilityzoneprofile_list(self, **kwargs): + """List all availabilityzone profiles + + :param kwargs: + Parameters to filter on + :return: + List of availabilityzone profile + """ + url = const.BASE_AVAILABILITYZONEPROFILE_URL + response = self._list(url, **kwargs) + + return response + + def availabilityzoneprofile_show(self, availabilityzoneprofile_id): + """Show a availabilityzone profile + + :param string availabilityzoneprofile_id: + ID of the availabilityzone profile to show + :return: + A dict of the specified availabilityzone profile's settings + """ + response = self._find(path=const.BASE_AVAILABILITYZONEPROFILE_URL, + value=availabilityzoneprofile_id) + + return response + + @correct_return_codes + def availabilityzoneprofile_set(self, availabilityzoneprofile_id, + **kwargs): + """Update a availabilityzone profile's settings + + :param string availabilityzoneprofile_id: + ID of the availabilityzone profile to update + :kwargs: + A dict of arguments to update the availabilityzone profile + :return: + Response Code from the API + """ + url = const.BASE_SINGLE_AVAILABILITYZONEPROFILE_URL.format( + uuid=availabilityzoneprofile_id) + response = self._create(url, method='PUT', **kwargs) + + return response + + @correct_return_codes + def availabilityzoneprofile_delete(self, availabilityzoneprofile_id): + """Delete a availabilityzone profile + + :param string availabilityzoneprofile_id: + ID of the availabilityzone profile to delete + :return: + Response Code from the API + """ + url = const.BASE_SINGLE_AVAILABILITYZONEPROFILE_URL.format( + uuid=availabilityzoneprofile_id) + response = self._delete(url) + + return response diff --git a/octaviaclient/osc/v2/availabilityzone.py b/octaviaclient/osc/v2/availabilityzone.py new file mode 100644 index 0000000..0063961 --- /dev/null +++ b/octaviaclient/osc/v2/availabilityzone.py @@ -0,0 +1,250 @@ +# Copyright (c) 2018 China Telecom Corporation +# Copyright 2019 Red Hat, Inc. All rights reserved. +# +# 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. + +"""Availabilityzone action implementation""" + +from cliff import lister +from osc_lib.command import command +from osc_lib import utils + +from octaviaclient.osc.v2 import constants as const +from octaviaclient.osc.v2 import utils as v2_utils + + +class CreateAvailabilityzone(command.ShowOne): + """Create an octavia availability zone""" + + def get_parser(self, prog_name): + parser = super(CreateAvailabilityzone, self).get_parser(prog_name) + + parser.add_argument( + '--name', + metavar='', + required=True, + help="New availability zone name." + ) + parser.add_argument( + '--availabilityzoneprofile', + metavar='', + required=True, + help="Availability zone profile to add the AZ to (name or ID)." + ) + parser.add_argument( + '--description', + metavar='', + help="Set the availability zone description." + ) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + default=None, + help="Enable the availability zone." + ) + admin_group.add_argument( + '--disable', + action='store_true', + default=None, + help="Disable the availability zone." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.AVAILABILITYZONE_ROWS + attrs = v2_utils.get_availabilityzone_attrs(self.app.client_manager, + parsed_args) + body = {"availability_zone": attrs} + data = self.app.client_manager.load_balancer.availabilityzone_create( + json=body) + + formatters = {'availability_zone_profiles': v2_utils.format_list} + + return (rows, + (utils.get_dict_properties( + data['availability_zone'], rows, formatters=formatters))) + + +class DeleteAvailabilityzone(command.Command): + """Delete an availability zone""" + + def get_parser(self, prog_name): + parser = super(DeleteAvailabilityzone, self).get_parser(prog_name) + + parser.add_argument( + 'availabilityzone', + metavar='', + help="Name of the availability zone to delete." + ) + + return parser + + def take_action(self, parsed_args): + attrs = v2_utils.get_availabilityzone_attrs(self.app.client_manager, + parsed_args) + availabilityzone_name = attrs.pop('availabilityzone_name') + + self.app.client_manager.load_balancer.availabilityzone_delete( + availabilityzone_name=availabilityzone_name) + + +class ListAvailabilityzone(lister.Lister): + """List availability zones""" + + def get_parser(self, prog_name): + parser = super(ListAvailabilityzone, self).get_parser(prog_name) + + parser.add_argument( + '--name', + metavar='', + help="List availability zones according to their name." + ) + parser.add_argument( + '--availabilityzoneprofile', + metavar='', + help="List availability zones according to their AZ profile.", + ) + admin_state_group = parser.add_mutually_exclusive_group() + admin_state_group.add_argument( + '--enable', + action='store_true', + default=None, + help="List enabled availability zones." + ) + admin_state_group.add_argument( + '--disable', + action='store_true', + default=None, + help="List disabled availability zones." + ) + + return parser + + def take_action(self, parsed_args): + columns = const.AVAILABILITYZONE_COLUMNS + attrs = v2_utils.get_availabilityzone_attrs(self.app.client_manager, + parsed_args) + data = self.app.client_manager.load_balancer.availabilityzone_list( + **attrs) + formatters = {'availabilityzoneprofiles': v2_utils.format_list} + return (columns, + (utils.get_dict_properties(s, columns, formatters=formatters) + for s in data['availability_zones'])) + + +class ShowAvailabilityzone(command.ShowOne): + """Show the details for a single availability zone""" + + def get_parser(self, prog_name): + parser = super(ShowAvailabilityzone, self).get_parser(prog_name) + + parser.add_argument( + 'availabilityzone', + metavar='', + help="Name of the availability zone." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.AVAILABILITYZONE_ROWS + attrs = v2_utils.get_availabilityzone_attrs(self.app.client_manager, + parsed_args) + availabilityzone_name = attrs.pop('availabilityzone_name') + + data = self.app.client_manager.load_balancer.availabilityzone_show( + availabilityzone_name=availabilityzone_name + ) + formatters = {'availabilityzoneprofiles': v2_utils.format_list} + + return (rows, (utils.get_dict_properties( + data, rows, formatters=formatters))) + + +class SetAvailabilityzone(command.Command): + """Update an availability zone""" + + def get_parser(self, prog_name): + parser = super(SetAvailabilityzone, self).get_parser(prog_name) + + parser.add_argument( + 'availabilityzone', + metavar='', + help='Name of the availability zone to update.' + ) + parser.add_argument( + '--description', + metavar='', + help="Set the description of the availability zone." + ) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + default=None, + help="Enable the availability zone." + ) + admin_group.add_argument( + '--disable', + action='store_true', + default=None, + help="Disable the availability zone." + ) + + return parser + + def take_action(self, parsed_args): + attrs = v2_utils.get_availabilityzone_attrs(self.app.client_manager, + parsed_args) + availabilityzone_name = attrs.pop('availabilityzone_name') + body = {'availability_zone': attrs} + + self.app.client_manager.load_balancer.availabilityzone_set( + availabilityzone_name, json=body) + + +class UnsetAvailabilityzone(command.Command): + """Clear availability zone settings""" + + def get_parser(self, prog_name): + parser = super(UnsetAvailabilityzone, self).get_parser(prog_name) + + parser.add_argument( + 'availabilityzone', + metavar='', + help="Name of the availability zone to update." + ) + parser.add_argument( + '--description', + action='store_true', + help="Clear the availability zone description." + ) + return parser + + def take_action(self, parsed_args): + unset_args = v2_utils.get_unsets(parsed_args) + if not len(unset_args): + return + + availabilityzone_id = v2_utils.get_resource_id( + self.app.client_manager.load_balancer.availabilityzone_list, + 'availability_zones', parsed_args.availabilityzone) + + body = {'availability_zone': unset_args} + + self.app.client_manager.load_balancer.availabilityzone_set( + availabilityzone_id, json=body) diff --git a/octaviaclient/osc/v2/availabilityzoneprofile.py b/octaviaclient/osc/v2/availabilityzoneprofile.py new file mode 100644 index 0000000..e12b4e0 --- /dev/null +++ b/octaviaclient/osc/v2/availabilityzoneprofile.py @@ -0,0 +1,188 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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. + +"""Availabilityzone profile action implementation""" + +from cliff import lister +from osc_lib.command import command +from osc_lib import utils + +from octaviaclient.osc.v2 import constants as const +from octaviaclient.osc.v2 import utils as v2_utils + + +class CreateAvailabilityzoneProfile(command.ShowOne): + """Create an octavia availability zone profile""" + + def get_parser(self, prog_name): + parser = super(CreateAvailabilityzoneProfile, self).get_parser( + prog_name) + + parser.add_argument( + '--name', + metavar='', + required=True, + help="New octavia availability zone profile name." + ) + parser.add_argument( + '--provider', + metavar='', + required=True, + help="Provider name for the availability zone profile." + ) + parser.add_argument( + '--availability-zone-data', + metavar='', + required=True, + help="The JSON string containing the availability zone metadata." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.AVAILABILITYZONEPROFILE_ROWS + attrs = v2_utils.get_availabilityzoneprofile_attrs( + self.app.client_manager, parsed_args) + body = {"availability_zone_profile": attrs} + client_manager = self.app.client_manager + data = client_manager.load_balancer.availabilityzoneprofile_create( + json=body) + + return (rows, + (utils.get_dict_properties( + data['availability_zone_profile'], rows, formatters={}))) + + +class DeleteAvailabilityzoneProfile(command.Command): + """Delete an availability zone profile""" + + def get_parser(self, prog_name): + parser = super(DeleteAvailabilityzoneProfile, self).get_parser( + prog_name) + + parser.add_argument( + 'availabilityzoneprofile', + metavar='', + help="Availability zone profile to delete (name or ID)" + ) + + return parser + + def take_action(self, parsed_args): + attrs = v2_utils.get_availabilityzoneprofile_attrs( + self.app.client_manager, parsed_args) + availabilityzoneprofile_id = attrs.pop('availability_zone_profile_id') + + self.app.client_manager.load_balancer.availabilityzoneprofile_delete( + availabilityzoneprofile_id=availabilityzoneprofile_id) + + +class ListAvailabilityzoneProfile(lister.Lister): + """List availability zone profiles""" + + def get_parser(self, prog_name): + parser = super(ListAvailabilityzoneProfile, self).get_parser(prog_name) + + parser.add_argument( + '--name', + metavar='', + help="List availabilityzone profiles by profile name." + ) + parser.add_argument( + '--provider', + metavar='', + help="List availability zone profiles according to their " + "provider.", + ) + + return parser + + def take_action(self, parsed_args): + columns = const.AVAILABILITYZONEPROFILE_COLUMNS + attrs = v2_utils.get_availabilityzoneprofile_attrs( + self.app.client_manager, parsed_args) + client_manager = self.app.client_manager + data = client_manager.load_balancer.availabilityzoneprofile_list( + **attrs) + return (columns, + (utils.get_dict_properties(s, columns, formatters={}) + for s in data['availability_zone_profiles'])) + + +class ShowAvailabilityzoneProfile(command.ShowOne): + """Show the details of a single availability zone profile""" + + def get_parser(self, prog_name): + parser = super(ShowAvailabilityzoneProfile, self).get_parser(prog_name) + + parser.add_argument( + 'availabilityzoneprofile', + metavar='', + help="Name or UUID of the availability zone profile to show." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.AVAILABILITYZONEPROFILE_ROWS + attrs = v2_utils.get_availabilityzoneprofile_attrs( + self.app.client_manager, parsed_args) + availabilityzoneprofile_id = attrs.pop('availability_zone_profile_id') + client_manager = self.app.client_manager + data = client_manager.load_balancer.availabilityzoneprofile_show( + availabilityzoneprofile_id=availabilityzoneprofile_id + ) + + return (rows, (utils.get_dict_properties( + data, rows, formatters={}))) + + +class SetAvailabilityzoneProfile(command.Command): + """Update an availability zone profile""" + + def get_parser(self, prog_name): + parser = super(SetAvailabilityzoneProfile, self).get_parser(prog_name) + + parser.add_argument( + 'availabilityzoneprofile', + metavar='', + help='Name or UUID of the availability zone profile to update.' + ) + parser.add_argument( + '--name', + metavar='', + help="Set the name of the availability zone profile." + ) + parser.add_argument( + '--provider', + metavar='', + help="Set the provider of the availability zone profile." + ) + parser.add_argument( + '--availabilityzone-data', + metavar='', + help="Set the availability zone data of the profile." + ) + + return parser + + def take_action(self, parsed_args): + attrs = v2_utils.get_availabilityzoneprofile_attrs( + self.app.client_manager, parsed_args) + availabilityzoneprofile_id = attrs.pop('availability_zone_profile_id') + body = {'availability_zone_profile': attrs} + + self.app.client_manager.load_balancer.availabilityzoneprofile_set( + availabilityzoneprofile_id, json=body) diff --git a/octaviaclient/osc/v2/constants.py b/octaviaclient/osc/v2/constants.py index ee44b6d..07b051f 100644 --- a/octaviaclient/osc/v2/constants.py +++ b/octaviaclient/osc/v2/constants.py @@ -14,6 +14,7 @@ LOAD_BALANCER_ROWS = ( 'admin_state_up', + 'availability_zone', 'created_at', 'description', 'flavor_id', @@ -281,7 +282,8 @@ PROVIDER_COLUMNS = ( 'description', ) -PROVIDER_CAPABILICY_COLUMNS = ( +PROVIDER_CAPABILITY_COLUMNS = ( + 'type', 'name', 'description', ) @@ -314,4 +316,30 @@ FLAVORPROFILE_COLUMNS = ( 'provider_name', ) +AVAILABILITYZONE_ROWS = ( + 'name', + 'availability_zone_profile_id', + 'enabled', + 'description', +) + +AVAILABILITYZONE_COLUMNS = ( + 'name', + 'availability_zone_profile_id', + 'enabled', +) + +AVAILABILITYZONEPROFILE_ROWS = ( + 'id', + 'name', + 'provider_name', + 'availability_zone_data' +) + +AVAILABILITYZONEPROFILE_COLUMNS = ( + 'id', + 'name', + 'provider_name', +) + PROVISIONING_STATUS = 'provisioning_status' diff --git a/octaviaclient/osc/v2/flavor.py b/octaviaclient/osc/v2/flavor.py index 38aff4e..84ca398 100644 --- a/octaviaclient/osc/v2/flavor.py +++ b/octaviaclient/osc/v2/flavor.py @@ -228,7 +228,7 @@ class SetFlavor(command.Command): class UnsetFlavor(command.Command): - """Clear health monitor settings""" + """Clear flavor settings""" def get_parser(self, prog_name): parser = super(UnsetFlavor, self).get_parser(prog_name) diff --git a/octaviaclient/osc/v2/load_balancer.py b/octaviaclient/osc/v2/load_balancer.py index 55efcb2..d3ead07 100644 --- a/octaviaclient/osc/v2/load_balancer.py +++ b/octaviaclient/osc/v2/load_balancer.py @@ -101,6 +101,13 @@ class CreateLoadBalancer(command.ShowOne): help="Provider name for the load balancer." ) + parser.add_argument( + '--availability-zone', + metavar='', + default=None, + help="Availability zone for the load balancer." + ) + admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', @@ -313,6 +320,11 @@ class ListLoadBalancer(lister.Lister): metavar='', help="List load balancers according to their flavor." ) + parser.add_argument( + '--availability-zone', + metavar='', + help="List load balancers according to their availability zone." + ) return parser diff --git a/octaviaclient/osc/v2/provider.py b/octaviaclient/osc/v2/provider.py index f659396..cf99c8e 100644 --- a/octaviaclient/osc/v2/provider.py +++ b/octaviaclient/osc/v2/provider.py @@ -41,11 +41,11 @@ class ListProvider(lister.Lister): ) for s in data['providers'])) -class ListProviderFlavorCapability(lister.Lister): - """List specified provider driver's flavor capabilicies.""" +class ListProviderCapability(lister.Lister): + """List specified provider driver's capabilities.""" def get_parser(self, prog_name): - parser = super(ListProviderFlavorCapability, + parser = super(ListProviderCapability, self).get_parser(prog_name) parser.add_argument( @@ -53,18 +53,48 @@ class ListProviderFlavorCapability(lister.Lister): metavar='', help="Name of the provider driver." ) + type_group = parser.add_mutually_exclusive_group() + type_group.add_argument( + '--flavor', + action='store_true', + default=None, + help="Get capabilities for flavor only." + ) + type_group.add_argument( + '--availability-zone', + action='store_true', + default=None, + help="Get capabilities for availability zone only." + ) return parser def take_action(self, parsed_args): - columns = const.PROVIDER_CAPABILICY_COLUMNS + columns = const.PROVIDER_CAPABILITY_COLUMNS attrs = v2_utils.get_provider_attrs(parsed_args) provider = attrs.pop('provider_name') + fetch_flavor = attrs.pop('flavor', False) + fetch_az = attrs.pop('availability_zone', False) client = self.app.client_manager - data = client.load_balancer.provider_capability_list( - provider=provider) + + data = [] + if not fetch_az: + flavor_data = ( + client.load_balancer. + provider_flavor_capability_list(provider=provider)) + for capability in flavor_data['flavor_capabilities']: + capability['type'] = 'flavor' + data.append(capability) + if not fetch_flavor: + az_data = ( + client.load_balancer. + provider_availability_zone_capability_list( + provider=provider)) + for capability in az_data['availability_zone_capabilities']: + capability['type'] = 'availability_zone' + data.append(capability) return (columns, (utils.get_dict_properties( s, columns, formatters={}, - ) for s in data['flavor_capabilities'])) + ) for s in data)) diff --git a/octaviaclient/osc/v2/utils.py b/octaviaclient/osc/v2/utils.py index 4d6e90f..899d66a 100644 --- a/octaviaclient/osc/v2/utils.py +++ b/octaviaclient/osc/v2/utils.py @@ -80,6 +80,11 @@ def get_resource_id(resource, resource_name, name): if name.lower() in ('none', 'null', 'void'): return None + primary_key = 'id' + # Availability-zones don't have an id value + if resource_name == 'availability_zones': + primary_key = 'name' + # Projects can be non-uuid so we need to account for this if resource_name == 'project': if name != 'non-uuid': @@ -116,7 +121,8 @@ def get_resource_id(resource, resource_name, name): name)) raise osc_exc.CommandError(msg) else: - return names[0].get('id') + return names[0].get(primary_key) + except IndexError: msg = "Unable to locate {0} in {1}".format(name, resource_name) raise osc_exc.CommandError(msg) @@ -171,6 +177,8 @@ def get_loadbalancer_attrs(client_manager, parsed_args): 'flavors', client_manager.load_balancer.flavor_list ), + 'availability_zone': ('availability_zone', str), + } _attrs = vars(parsed_args) @@ -471,6 +479,8 @@ def get_provider_attrs(parsed_args): attr_map = { 'provider': ('provider_name', str), 'description': ('description', str), + 'flavor': ('flavor', bool), + 'availability_zone': ('availability_zone', bool), } return _map_attrs(vars(parsed_args), attr_map) @@ -518,6 +528,48 @@ def get_flavorprofile_attrs(client_manager, parsed_args): return attrs +def get_availabilityzone_attrs(client_manager, parsed_args): + attr_map = { + 'name': ('name', str), + 'availabilityzone': ( + 'availabilityzone_name', + 'availability_zones', + client_manager.load_balancer.availabilityzone_list, + ), + 'availabilityzoneprofile': ( + 'availability_zone_profile_id', + 'availability_zone_profiles', + client_manager.load_balancer.availabilityzoneprofile_list, + ), + 'enable': ('enabled', lambda x: True), + 'disable': ('enabled', lambda x: False), + 'description': ('description', str), + } + + _attrs = vars(parsed_args) + attrs = _map_attrs(_attrs, attr_map) + + return attrs + + +def get_availabilityzoneprofile_attrs(client_manager, parsed_args): + attr_map = { + 'name': ('name', str), + 'availabilityzoneprofile': ( + 'availability_zone_profile_id', + 'availability_zone_profiles', + client_manager.load_balancer.availabilityzoneprofile_list, + ), + 'provider': ('provider_name', str), + 'availability_zone_data': ('availability_zone_data', str), + } + + _attrs = vars(parsed_args) + attrs = _map_attrs(_attrs, attr_map) + + return attrs + + def format_list(data): return '\n'.join(i['id'] for i in data) diff --git a/octaviaclient/tests/unit/api/test_octavia.py b/octaviaclient/tests/unit/api/test_octavia.py index 19562f8..2be707c 100644 --- a/octaviaclient/tests/unit/api/test_octavia.py +++ b/octaviaclient/tests/unit/api/test_octavia.py @@ -40,6 +40,8 @@ FAKE_AMP = uuidutils.generate_uuid() FAKE_PROVIDER = 'fake_provider' FAKE_FV = uuidutils.generate_uuid() FAKE_FVPF = uuidutils.generate_uuid() +FAKE_AZ = 'fake_az' +FAKE_AZPF = uuidutils.generate_uuid() LIST_LB_RESP = { @@ -115,6 +117,16 @@ LIST_FVPF_RESP = { {'name': 'fvpf2'}] } +LIST_AZ_RESP = { + 'availability_zones': [{'name': 'az1'}, + {'name': 'az2'}] +} + +LIST_AZPF_RESP = { + 'availability_zone_profiles': [{'name': 'azpf1'}, + {'name': 'azpf2'}] +} + SINGLE_LB_RESP = {'loadbalancer': {'id': FAKE_LB, 'name': 'lb1'}} SINGLE_LB_UPDATE = {"loadbalancer": {"admin_state_up": False}} SINGLE_LB_STATS_RESP = {'bytes_in': '0'} @@ -154,6 +166,14 @@ SINGLE_FV_UPDATE = {'flavor': {'enabled': False}} SINGLE_FVPF_RESP = {'flavorprofile': {'id': FAKE_FVPF, 'name': 'fvpf1'}} SINGLE_FVPF_UPDATE = {'flavorprofile': {'provider_name': 'fake_provider'}} +SINGLE_AZ_RESP = {'availability_zone': {'name': FAKE_AZ}} +SINGLE_AZ_UPDATE = {'availability_zone': {'enabled': False}} + +SINGLE_AZPF_RESP = {'availability_zone_profile': {'id': FAKE_AZPF, + 'name': 'azpf1'}} +SINGLE_AZPF_UPDATE = {'availability_zone_profile': { + 'provider_name': 'fake_provider'}} + class TestAPI(utils.TestCase): def test_client_exception(self): @@ -968,7 +988,7 @@ class TestLoadBalancer(TestOctaviaClient): ret = self.api.provider_list() self.assertEqual(LIST_PROVIDER_RESP, ret) - def test_show_provider_capabilicy(self): + def test_show_provider_capability(self): self.requests_mock.register_uri( 'GET', (FAKE_LBAAS_URL + 'providers/' + @@ -976,7 +996,7 @@ class TestLoadBalancer(TestOctaviaClient): json=SINGLE_PROVIDER_CAPABILITY_RESP, status_code=200 ) - ret = self.api.provider_capability_list(FAKE_PROVIDER) + ret = self.api.provider_flavor_capability_list(FAKE_PROVIDER) self.assertEqual( SINGLE_PROVIDER_CAPABILITY_RESP, ret) @@ -1150,3 +1170,175 @@ class TestLoadBalancer(TestOctaviaClient): self._error_message, self.api.flavorprofile_delete, FAKE_FVPF) + + def test_list_availabilityzone_no_options(self): + self.requests_mock.register_uri( + 'GET', + FAKE_LBAAS_URL + 'availabilityzones', + json=LIST_AZ_RESP, + status_code=200, + ) + ret = self.api.availabilityzone_list() + self.assertEqual(LIST_AZ_RESP, ret) + + def test_show_availabilityzone(self): + self.requests_mock.register_uri( + 'GET', + FAKE_LBAAS_URL + 'availabilityzones/' + FAKE_AZ, + json=SINGLE_AZ_RESP, + status_code=200 + ) + ret = self.api.availabilityzone_show(FAKE_AZ) + self.assertEqual(SINGLE_AZ_RESP['availability_zone'], ret) + + def test_create_availabilityzone(self): + self.requests_mock.register_uri( + 'POST', + FAKE_LBAAS_URL + 'availabilityzones', + json=SINGLE_AZ_RESP, + status_code=200 + ) + ret = self.api.availabilityzone_create(json=SINGLE_AZ_RESP) + self.assertEqual(SINGLE_AZ_RESP, ret) + + def test_create_availabilityzone_error(self): + self.requests_mock.register_uri( + 'POST', + FAKE_LBAAS_URL + 'availabilityzones', + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(exceptions.OctaviaClientException, + self._error_message, + self.api.availabilityzone_create, + json=SINGLE_AZ_RESP) + + def test_set_availabilityzone(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_LBAAS_URL + 'availabilityzones/' + FAKE_AZ, + json=SINGLE_AZ_UPDATE, + status_code=200 + ) + ret = self.api.availabilityzone_set(FAKE_AZ, json=SINGLE_AZ_UPDATE) + self.assertEqual(SINGLE_AZ_UPDATE, ret) + + def test_set_availabilityzone_error(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_LBAAS_URL + 'availabilityzones/' + FAKE_AZ, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(exceptions.OctaviaClientException, + self._error_message, + self.api.availabilityzone_set, + FAKE_AZ, + json=SINGLE_AZ_UPDATE) + + def test_delete_availabilityzone(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_LBAAS_URL + 'availabilityzones/' + FAKE_AZ, + status_code=200 + ) + ret = self.api.availabilityzone_delete(FAKE_AZ) + self.assertEqual(200, ret.status_code) + + def test_delete_availabilityzone_error(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_LBAAS_URL + 'availabilityzones/' + FAKE_AZ, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(exceptions.OctaviaClientException, + self._error_message, + self.api.availabilityzone_delete, + FAKE_AZ) + + def test_list_availabilityzoneprofiles_no_options(self): + self.requests_mock.register_uri( + 'GET', + FAKE_LBAAS_URL + 'availabilityzoneprofiles', + json=LIST_AZPF_RESP, + status_code=200, + ) + ret = self.api.availabilityzoneprofile_list() + self.assertEqual(LIST_AZPF_RESP, ret) + + def test_show_availabilityzoneprofile(self): + self.requests_mock.register_uri( + 'GET', + FAKE_LBAAS_URL + 'availabilityzoneprofiles/' + FAKE_AZPF, + json=SINGLE_AZPF_RESP, + status_code=200 + ) + ret = self.api.availabilityzoneprofile_show(FAKE_AZPF) + self.assertEqual(SINGLE_AZPF_RESP['availability_zone_profile'], ret) + + def test_create_availabilityzoneprofile(self): + self.requests_mock.register_uri( + 'POST', + FAKE_LBAAS_URL + 'availabilityzoneprofiles', + json=SINGLE_AZPF_RESP, + status_code=200 + ) + ret = self.api.availabilityzoneprofile_create(json=SINGLE_AZPF_RESP) + self.assertEqual(SINGLE_AZPF_RESP, ret) + + def test_create_availabilityzoneprofile_error(self): + self.requests_mock.register_uri( + 'POST', + FAKE_LBAAS_URL + 'availabilityzoneprofiles', + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(exceptions.OctaviaClientException, + self._error_message, + self.api.availabilityzoneprofile_create, + json=SINGLE_AZPF_RESP) + + def test_set_availabilityzoneprofiles(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_LBAAS_URL + 'availabilityzoneprofiles/' + FAKE_AZPF, + json=SINGLE_AZPF_UPDATE, + status_code=200 + ) + ret = self.api.availabilityzoneprofile_set(FAKE_AZPF, + json=SINGLE_AZPF_UPDATE) + self.assertEqual(SINGLE_AZPF_UPDATE, ret) + + def test_set_availabilityzoneprofiles_error(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_LBAAS_URL + 'availabilityzoneprofiles/' + FAKE_AZPF, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(exceptions.OctaviaClientException, + self._error_message, + self.api.availabilityzoneprofile_set, + FAKE_AZPF, json=SINGLE_AZPF_UPDATE) + + def test_delete_availabilityzoneprofile(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_LBAAS_URL + 'availabilityzoneprofiles/' + FAKE_AZPF, + status_code=200 + ) + ret = self.api.availabilityzoneprofile_delete(FAKE_AZPF) + self.assertEqual(200, ret.status_code) + + def test_delete_availabilityzoneprofile_error(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_LBAAS_URL + 'availabilityzoneprofiles/' + FAKE_AZPF, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(exceptions.OctaviaClientException, + self._error_message, + self.api.availabilityzoneprofile_delete, + FAKE_AZPF) diff --git a/octaviaclient/tests/unit/osc/v2/constants.py b/octaviaclient/tests/unit/osc/v2/constants.py index dfb9718..7821c5e 100644 --- a/octaviaclient/tests/unit/osc/v2/constants.py +++ b/octaviaclient/tests/unit/osc/v2/constants.py @@ -171,8 +171,8 @@ PROVIDER_ATTRS = { } CAPABILITY_ATTRS = { - "name": "some_capabilicy", - "description": "Description of capabilicy." + "name": "some_capability", + "description": "Description of capability." } FLAVOR_ATTRS = { @@ -188,3 +188,17 @@ FLAVORPROFILE_ATTRS = { "provider_name": "mock_provider", "flavor_data": '{"mock_key": "mock_value"}', } + +AVAILABILITY_ZONE_ATTRS = { + "name": "az-name-" + uuidutils.generate_uuid(dashed=True), + "availability_zone_profile_id": None, + "enabled": True, + "description": "Description of AZ", +} + +AVAILABILITY_ZONE_PROFILE_ATTRS = { + "id": uuidutils.generate_uuid(), + "name": "azpf-name-" + uuidutils.generate_uuid(dashed=True), + "provider_name": "mock_provider", + "availabilityzone_data": '{"mock_key": "mock_value"}', +} diff --git a/octaviaclient/tests/unit/osc/v2/test_availabilityzone.py b/octaviaclient/tests/unit/osc/v2/test_availabilityzone.py new file mode 100644 index 0000000..c746b1d --- /dev/null +++ b/octaviaclient/tests/unit/osc/v2/test_availabilityzone.py @@ -0,0 +1,228 @@ +# +# 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. + +import copy +import mock + +from osc_lib import exceptions + +from octaviaclient.osc.v2 import availabilityzone +from octaviaclient.osc.v2 import constants +from octaviaclient.tests.unit.osc.v2 import constants as attr_consts +from octaviaclient.tests.unit.osc.v2 import fakes + + +class TestAvailabilityzone(fakes.TestOctaviaClient): + + def setUp(self): + super(TestAvailabilityzone, self).setUp() + + self._availabilityzone = fakes.createFakeResource('availability_zone') + self.availabilityzone_info = copy.deepcopy( + attr_consts.AVAILABILITY_ZONE_ATTRS) + self.columns = copy.deepcopy(constants.AVAILABILITYZONE_COLUMNS) + + self.api_mock = mock.Mock() + self.api_mock.availabilityzone_list.return_value = copy.deepcopy( + {'availability_zones': [attr_consts.AVAILABILITY_ZONE_ATTRS]}) + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + +class TestAvailabilityzoneList(TestAvailabilityzone): + + def setUp(self): + super(TestAvailabilityzoneList, self).setUp() + self.datalist = (tuple( + attr_consts.AVAILABILITY_ZONE_ATTRS[k] for k in self.columns),) + self.cmd = availabilityzone.ListAvailabilityzone(self.app, None) + + def test_availabilityzone_list_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.api_mock.availabilityzone_list.assert_called_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_availabilityzone_list_with_options(self): + arglist = ['--name', 'availabilityzone1'] + verifylist = [('name', 'availabilityzone1')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.api_mock.availabilityzone_list.assert_called_with( + name='availabilityzone1') + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + +class TestAvailabilityzoneDelete(TestAvailabilityzone): + + def setUp(self): + super(TestAvailabilityzoneDelete, self).setUp() + self.cmd = availabilityzone.DeleteAvailabilityzone(self.app, None) + + def test_availabilityzone_delete(self): + arglist = [self._availabilityzone.name] + verifylist = [ + ('availabilityzone', self._availabilityzone.name) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzone_delete.assert_called_with( + availabilityzone_name=self._availabilityzone.name) + + def test_availabilityzone_delete_failure(self): + arglist = ['unknown_availabilityzone'] + verifylist = [ + ('availabilityzone', 'unknown_availabilityzone') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + self.assertNotCalled(self.api_mock.availabilityzone_delete) + + +class TestAvailabilityzoneCreate(TestAvailabilityzone): + + def setUp(self): + super(TestAvailabilityzoneCreate, self).setUp() + self.api_mock.availabilityzone_create.return_value = { + 'availability_zone': self.availabilityzone_info} + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = availabilityzone.CreateAvailabilityzone(self.app, None) + + @mock.patch('octaviaclient.osc.v2.utils.get_availabilityzone_attrs') + def test_availabilityzone_create(self, mock_client): + mock_client.return_value = self.availabilityzone_info + arglist = ['--name', self._availabilityzone.name, + '--availabilityzoneprofile', 'mock_azpf_id', + '--description', 'description for availabilityzone'] + verifylist = [ + ('availabilityzoneprofile', 'mock_azpf_id'), + ('name', self._availabilityzone.name), + ('description', 'description for availabilityzone') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzone_create.assert_called_with( + json={'availability_zone': self.availabilityzone_info}) + + +class TestAvailabilityzoneShow(TestAvailabilityzone): + + def setUp(self): + super(TestAvailabilityzoneShow, self).setUp() + mock_show = self.api_mock.availabilityzone_show + mock_show.return_value = self.availabilityzone_info + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = availabilityzone.ShowAvailabilityzone(self.app, None) + + def test_availabilityzone_show(self): + arglist = [self._availabilityzone.name] + verifylist = [ + ('availabilityzone', self._availabilityzone.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzone_show.assert_called_with( + availabilityzone_name=self._availabilityzone.name) + + +class TestAvailabilityzoneSet(TestAvailabilityzone): + + def setUp(self): + super(TestAvailabilityzoneSet, self).setUp() + self.cmd = availabilityzone.SetAvailabilityzone(self.app, None) + + def test_availabilityzone_set(self): + arglist = [self._availabilityzone.name, '--description', 'new_desc'] + verifylist = [ + ('availabilityzone', self._availabilityzone.name), + ('description', 'new_desc'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzone_set.assert_called_with( + self._availabilityzone.name, json={ + 'availability_zone': { + 'description': 'new_desc' + }}) + + +class TestAvailabilityzoneUnset(TestAvailabilityzone): + PARAMETERS = ('description',) + + def setUp(self): + super(TestAvailabilityzoneUnset, self).setUp() + self.cmd = availabilityzone.UnsetAvailabilityzone(self.app, None) + + def test_hm_unset_description(self): + self._test_availabilityzone_unset_param('description') + + def _test_availabilityzone_unset_param(self, param): + self.api_mock.availabilityzone_set.reset_mock() + arg_param = param.replace('_', '-') if '_' in param else param + arglist = [self._availabilityzone.name, '--%s' % arg_param] + ref_body = {'availability_zone': {param: None}} + verifylist = [ + ('availabilityzone', self._availabilityzone.name), + ] + for ref_param in self.PARAMETERS: + verifylist.append((ref_param, param == ref_param)) + print(verifylist) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzone_set.assert_called_once_with( + self._availabilityzone.name, json=ref_body) + + def test_availabilityzone_unset_all(self): + self.api_mock.availabilityzone_set.reset_mock() + ref_body = {'availability_zone': {x: None for x in self.PARAMETERS}} + arglist = [self._availabilityzone.name] + for ref_param in self.PARAMETERS: + arg_param = (ref_param.replace('_', '-') if '_' in ref_param else + ref_param) + arglist.append('--%s' % arg_param) + verifylist = list(zip(self.PARAMETERS, [True] * len(self.PARAMETERS))) + verifylist = [('availabilityzone', + self._availabilityzone.name)] + verifylist + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzone_set.assert_called_once_with( + self._availabilityzone.name, json=ref_body) + + def test_availabilityzone_unset_none(self): + self.api_mock.availabilityzone_set.reset_mock() + arglist = [self._availabilityzone.name] + verifylist = list(zip(self.PARAMETERS, [False] * len(self.PARAMETERS))) + verifylist = [('availabilityzone', + self._availabilityzone.name)] + verifylist + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzone_set.assert_not_called() diff --git a/octaviaclient/tests/unit/osc/v2/test_availabilityzoneprofile.py b/octaviaclient/tests/unit/osc/v2/test_availabilityzoneprofile.py new file mode 100644 index 0000000..873ffb8 --- /dev/null +++ b/octaviaclient/tests/unit/osc/v2/test_availabilityzoneprofile.py @@ -0,0 +1,185 @@ +# +# 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. + +import copy +import mock + +from osc_lib import exceptions + +from octaviaclient.osc.v2 import availabilityzoneprofile +from octaviaclient.osc.v2 import constants +from octaviaclient.tests.unit.osc.v2 import constants as attr_consts +from octaviaclient.tests.unit.osc.v2 import fakes + + +class TestAvailabilityzoneProfile(fakes.TestOctaviaClient): + + def setUp(self): + super(TestAvailabilityzoneProfile, self).setUp() + + self._availabilityzoneprofile = fakes.createFakeResource( + 'availability_zone_profile') + self.availabilityzoneprofile_info = copy.deepcopy( + attr_consts.AVAILABILITY_ZONE_PROFILE_ATTRS) + self.columns = copy.deepcopy(constants.AVAILABILITYZONEPROFILE_COLUMNS) + + self.api_mock = mock.Mock() + mock_list = self.api_mock.availabilityzoneprofile_list + mock_list.return_value = copy.deepcopy({'availability_zone_profiles': [ + attr_consts.AVAILABILITY_ZONE_PROFILE_ATTRS]}) + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + +class TestAvailabilityzoneProfileList(TestAvailabilityzoneProfile): + + def setUp(self): + super(TestAvailabilityzoneProfileList, self).setUp() + self.datalist = (tuple( + attr_consts.AVAILABILITY_ZONE_PROFILE_ATTRS[k] + for k in self.columns),) + self.cmd = availabilityzoneprofile.ListAvailabilityzoneProfile( + self.app, None) + + def test_availabilityzoneprofile_list_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.api_mock.availabilityzoneprofile_list.assert_called_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_availabilityzoneprofile_list_with_options(self): + arglist = ['--name', 'availabilityzoneprofile1'] + verifylist = [('name', 'availabilityzoneprofile1')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.api_mock.availabilityzoneprofile_list.assert_called_with( + name='availabilityzoneprofile1') + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + +class TestAvailabilityzoneProfileDelete(TestAvailabilityzoneProfile): + + def setUp(self): + super(TestAvailabilityzoneProfileDelete, self).setUp() + self.cmd = availabilityzoneprofile.DeleteAvailabilityzoneProfile( + self.app, None) + + def test_availabilityzoneprofile_delete(self): + arglist = [self._availabilityzoneprofile.id] + verifylist = [ + ('availabilityzoneprofile', self._availabilityzoneprofile.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzoneprofile_delete.assert_called_with( + availabilityzoneprofile_id=self._availabilityzoneprofile.id) + + def test_availabilityzoneprofile_delete_failure(self): + arglist = ['unknown_availabilityzoneprofile'] + verifylist = [ + ('availabilityzoneprofile', 'unknown_availabilityzoneprofile') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + self.assertNotCalled(self.api_mock.availabilityzoneprofile_delete) + + +class TestAvailabilityzoneProfileCreate(TestAvailabilityzoneProfile): + + def setUp(self): + super(TestAvailabilityzoneProfileCreate, self).setUp() + self.api_mock.availabilityzoneprofile_create.return_value = { + 'availability_zone_profile': self.availabilityzoneprofile_info} + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = availabilityzoneprofile.CreateAvailabilityzoneProfile( + self.app, None) + + @mock.patch('octaviaclient.osc.v2.utils.get_availabilityzoneprofile_attrs') + def test_availabilityzoneprofile_create(self, mock_client): + mock_client.return_value = self.availabilityzoneprofile_info + arglist = ['--name', self._availabilityzoneprofile.name, + '--provider', 'mock_provider', + '--availability-zone-data', '{"mock_key": "mock_value"}'] + verifylist = [ + ('provider', 'mock_provider'), + ('name', self._availabilityzoneprofile.name), + ('availability_zone_data', '{"mock_key": "mock_value"}') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzoneprofile_create.assert_called_with( + json={ + 'availability_zone_profile': self.availabilityzoneprofile_info + }) + + +class TestAvailabilityzoneProfileShow(TestAvailabilityzoneProfile): + + def setUp(self): + super(TestAvailabilityzoneProfileShow, self).setUp() + mock_show = self.api_mock.availabilityzoneprofile_show + mock_show.return_value = self.availabilityzoneprofile_info + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = availabilityzoneprofile.ShowAvailabilityzoneProfile( + self.app, None) + + def test_availabilityzoneprofile_show(self): + arglist = [self._availabilityzoneprofile.id] + verifylist = [ + ('availabilityzoneprofile', self._availabilityzoneprofile.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzoneprofile_show.assert_called_with( + availabilityzoneprofile_id=self._availabilityzoneprofile.id) + + +class TestAvailabilityzoneProfileSet(TestAvailabilityzoneProfile): + + def setUp(self): + super(TestAvailabilityzoneProfileSet, self).setUp() + self.cmd = availabilityzoneprofile.SetAvailabilityzoneProfile( + self.app, None) + + def test_availabilityzoneprofile_set(self): + arglist = [self._availabilityzoneprofile.id, '--name', 'new_name'] + verifylist = [ + ('availabilityzoneprofile', self._availabilityzoneprofile.id), + ('name', 'new_name'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.availabilityzoneprofile_set.assert_called_with( + self._availabilityzoneprofile.id, json={ + 'availability_zone_profile': { + 'name': 'new_name' + }}) diff --git a/octaviaclient/tests/unit/osc/v2/test_provider.py b/octaviaclient/tests/unit/osc/v2/test_provider.py index 66e9b74..053ec4a 100644 --- a/octaviaclient/tests/unit/osc/v2/test_provider.py +++ b/octaviaclient/tests/unit/osc/v2/test_provider.py @@ -62,8 +62,14 @@ class TestProviderCapability(fakes.TestOctaviaClient): super(TestProviderCapability, self).setUp() self.api_mock = mock.Mock() - self.api_mock.provider_capability_list.return_value = copy.deepcopy( - {'flavor_capabilities': [attr_consts.CAPABILITY_ATTRS]}) + self.api_mock.provider_flavor_capability_list.return_value = ( + copy.deepcopy( + {'flavor_capabilities': [attr_consts.CAPABILITY_ATTRS]})) + (self.api_mock.provider_availability_zone_capability_list. + return_value) = ( + copy.deepcopy( + {'availability_zone_capabilities': [ + attr_consts.CAPABILITY_ATTRS]})) lb_client = self.app.client_manager lb_client.load_balancer = self.api_mock @@ -75,15 +81,61 @@ class TestProviderCapabilityShow(TestProviderCapability): lb_client = self.app.client_manager lb_client.load_balancer = self.api_mock - self.cmd = provider.ListProviderFlavorCapability(self.app, None) + self.cmd = provider.ListProviderCapability(self.app, None) - def test_provider_capability_list(self): + def test_provider_capability_list_flavor(self): + arglist = ['--flavor', 'provider1'] + verifylist = [ + ('provider', 'provider1'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + capabilities = list(result[1]) + self.api_mock.provider_flavor_capability_list.assert_called_with( + provider='provider1') + (self.api_mock.provider_availability_zone_capability_list. + assert_not_called()) + self.assertIn( + tuple(['flavor'] + list(attr_consts.CAPABILITY_ATTRS.values())), + capabilities) + + def test_provider_capability_list_availability_zone(self): + arglist = ['--availability-zone', 'provider1'] + verifylist = [ + ('provider', 'provider1'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + capabilities = list(result[1]) + self.api_mock.provider_flavor_capability_list.assert_not_called() + (self.api_mock.provider_availability_zone_capability_list. + assert_called_with(provider='provider1')) + self.assertIn( + tuple( + ['availability_zone'] + + list(attr_consts.CAPABILITY_ATTRS.values())), + capabilities) + + def test_provider_capability_list_all(self): arglist = ['provider1'] verifylist = [ ('provider', 'provider1'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - self.api_mock.provider_capability_list.assert_called_with( + result = self.cmd.take_action(parsed_args) + capabilities = list(result[1]) + self.api_mock.provider_flavor_capability_list.assert_called_with( provider='provider1') + (self.api_mock.provider_availability_zone_capability_list. + assert_called_with(provider='provider1')) + self.assertIn( + tuple(['flavor'] + list(attr_consts.CAPABILITY_ATTRS.values())), + capabilities) + self.assertIn( + tuple( + ['availability_zone'] + + list(attr_consts.CAPABILITY_ATTRS.values())), + capabilities) diff --git a/releasenotes/notes/add-az-and-profiles-ed79c945c4e0d418.yaml b/releasenotes/notes/add-az-and-profiles-ed79c945c4e0d418.yaml new file mode 100644 index 0000000..beef560 --- /dev/null +++ b/releasenotes/notes/add-az-and-profiles-ed79c945c4e0d418.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added management of availability zone and availability zone profiles. + Creating a load balanacer now takes an optional availability-zone argument. + + Provider capability list now displays capabilities for both AZ and flavor, + and includes an extra column ``type``. It can be filtered by passing + ``--flavor`` or ``--availability-zone``. \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 64d59c1..a595279 100644 --- a/setup.cfg +++ b/setup.cfg @@ -85,7 +85,7 @@ openstack.load_balancer.v2 = loadbalancer_amphora_configure = octaviaclient.osc.v2.amphora:ConfigureAmphora loadbalancer_amphora_failover = octaviaclient.osc.v2.amphora:FailoverAmphora loadbalancer_provider_list = octaviaclient.osc.v2.provider:ListProvider - loadbalancer_provider_capability_list = octaviaclient.osc.v2.provider:ListProviderFlavorCapability + loadbalancer_provider_capability_list = octaviaclient.osc.v2.provider:ListProviderCapability loadbalancer_flavorprofile_create = octaviaclient.osc.v2.flavorprofile:CreateFlavorProfile loadbalancer_flavorprofile_list = octaviaclient.osc.v2.flavorprofile:ListFlavorProfile loadbalancer_flavorprofile_delete = octaviaclient.osc.v2.flavorprofile:DeleteFlavorProfile @@ -97,6 +97,17 @@ openstack.load_balancer.v2 = loadbalancer_flavor_show = octaviaclient.osc.v2.flavor:ShowFlavor loadbalancer_flavor_set = octaviaclient.osc.v2.flavor:SetFlavor loadbalancer_flavor_unset = octaviaclient.osc.v2.flavor:UnsetFlavor + loadbalancer_availabilityzoneprofile_create = octaviaclient.osc.v2.availabilityzoneprofile:CreateAvailabilityzoneProfile + loadbalancer_availabilityzoneprofile_list = octaviaclient.osc.v2.availabilityzoneprofile:ListAvailabilityzoneProfile + loadbalancer_availabilityzoneprofile_delete = octaviaclient.osc.v2.availabilityzoneprofile:DeleteAvailabilityzoneProfile + loadbalancer_availabilityzoneprofile_show = octaviaclient.osc.v2.availabilityzoneprofile:ShowAvailabilityzoneProfile + loadbalancer_availabilityzoneprofile_set = octaviaclient.osc.v2.availabilityzoneprofile:SetAvailabilityzoneProfile + loadbalancer_availabilityzone_create = octaviaclient.osc.v2.availabilityzone:CreateAvailabilityzone + loadbalancer_availabilityzone_list = octaviaclient.osc.v2.availabilityzone:ListAvailabilityzone + loadbalancer_availabilityzone_delete = octaviaclient.osc.v2.availabilityzone:DeleteAvailabilityzone + loadbalancer_availabilityzone_show = octaviaclient.osc.v2.availabilityzone:ShowAvailabilityzone + loadbalancer_availabilityzone_set = octaviaclient.osc.v2.availabilityzone:SetAvailabilityzone + loadbalancer_availabilityzone_unset = octaviaclient.osc.v2.availabilityzone:UnsetAvailabilityzone [build_sphinx] source-dir = doc/source