diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 2a4dd7b..edbce0d 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -94,3 +94,17 @@ provider .. autoprogram-cliff:: openstack.load_balancer.v2 :command: loadbalancer provider * + +====== +flavor +====== + +.. autoprogram-cliff:: openstack.load_balancer.v2 + :command: loadbalancer flavor * + +============= +flavorprofile +============= + +.. autoprogram-cliff:: openstack.load_balancer.v2 + :command: loadbalancer flavorprofile * diff --git a/octaviaclient/api/constants.py b/octaviaclient/api/constants.py index 1556f4c..025fb02 100644 --- a/octaviaclient/api/constants.py +++ b/octaviaclient/api/constants.py @@ -50,3 +50,8 @@ BASE_PROVIDER_URL = BASE_LBAAS_ENDPOINT + "/providers" BASE_PROVIDER_FLAVOR_CAPABILITY_URL = (BASE_LBAAS_ENDPOINT + "/providers/{provider}/" "flavor_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}" diff --git a/octaviaclient/api/v2/octavia.py b/octaviaclient/api/v2/octavia.py index aa6c84a..fd2ba17 100644 --- a/octaviaclient/api/v2/octavia.py +++ b/octaviaclient/api/v2/octavia.py @@ -752,6 +752,145 @@ class OctaviaAPI(api.BaseAPI): return response + def flavor_list(self, **kwargs): + """List all flavors + + :param kwargs: + Parameters to filter on + :return: + A ``dict`` containing a list of flavor + """ + url = const.BASE_FLAVOR_URL + response = self.list(path=url, **kwargs) + + return response + + @correct_return_codes + def flavor_delete(self, flavor_id): + """Delete a flavor + + :param string flavor_id: + ID of the flavor to delete + :return: + Response Code from the API + """ + url = const.BASE_SINGLE_FLAVOR_URL.format(uuid=flavor_id) + response = self.delete(url) + + return response + + @correct_return_codes + def flavor_create(self, **kwargs): + """Create a flavor + + :param kwargs: + Parameters to create a flavor with (expects json=) + :return: + A dict of the created flavor's settings + """ + url = const.BASE_FLAVOR_URL + response = self.create(url, **kwargs) + + return response + + @correct_return_codes + def flavor_set(self, flavor_id, **kwargs): + """Update a flavor's settings + + :param string flavor_id: + ID of the flavor to update + :param kwargs: + A dict of arguments to update a flavor + :return: + Response Code from the API + """ + url = const.BASE_SINGLE_FLAVOR_URL.format(uuid=flavor_id) + response = self.create(url, method='PUT', **kwargs) + + return response + + def flavor_show(self, flavor_id): + """Show a flavor + + :param string flavor_id: + ID of the flavor to show + :return: + A dict of the specified flavor's settings + """ + response = self.find(path=const.BASE_FLAVOR_URL, value=flavor_id) + + return response + + @correct_return_codes + def flavorprofile_create(self, **kwargs): + """Create a flavor profile + + :param kwargs: + Parameters to create a flavor profile with (expects json=) + :return: + A dict of the created flavor profile's settings + """ + url = const.BASE_FLAVORPROFILE_URL + response = self.create(url, **kwargs) + + return response + + def flavorprofile_list(self, **kwargs): + """List all flavor profiles + + :param kwargs: + Parameters to filter on + :return: + List of flavor profile + """ + url = const.BASE_FLAVORPROFILE_URL + response = self.list(url, **kwargs) + + return response + + def flavorprofile_show(self, flavorprofile_id): + """Show a flavor profile + + :param string flavorprofile_id: + ID of the flavor profile to show + :return: + A dict of the specified flavor profile's settings + """ + response = self.find(path=const.BASE_FLAVORPROFILE_URL, + value=flavorprofile_id) + + return response + + @correct_return_codes + def flavorprofile_set(self, flavorprofile_id, **kwargs): + """Update a flavor profile's settings + + :param string flavorprofile_id: + ID of the flavor profile to update + :kwargs: + A dict of arguments to update the flavor profile + :return: + Response Code from the API + """ + url = const.BASE_SINGLE_FLAVORPROFILE_URL.format(uuid=flavorprofile_id) + response = self.create(url, method='PUT', **kwargs) + + return response + + @correct_return_codes + def flavorprofile_delete(self, flavorprofile_id): + """Delete a flavor profile + + :param string flavorprofile_id: + ID of the flavor profile to delete + :return: + Response Code from the API + """ + url = const.BASE_SINGLE_FLAVORPROFILE_URL.format(uuid=flavorprofile_id) + response = self.delete(url) + + return response + class OctaviaClientException(Exception): """The base exception class for all exceptions this library raises.""" diff --git a/octaviaclient/osc/v2/constants.py b/octaviaclient/osc/v2/constants.py index 8e14152..cc516b9 100644 --- a/octaviaclient/osc/v2/constants.py +++ b/octaviaclient/osc/v2/constants.py @@ -274,3 +274,31 @@ PROVIDER_CAPABILICY_COLUMNS = ( 'name', 'description', ) + +FLAVOR_ROWS = ( + 'id', + 'name', + 'flavor_profile_id', + 'enabled', + 'description', +) + +FLAVOR_COLUMNS = ( + 'id', + 'name', + 'flavor_profile_id', + 'enabled', +) + +FLAVORPROFILE_ROWS = ( + 'id', + 'name', + 'provider_name', + 'flavor_data' +) + +FLAVORPROFILE_COLUMNS = ( + 'id', + 'name', + 'provider_name', +) diff --git a/octaviaclient/osc/v2/flavor.py b/octaviaclient/osc/v2/flavor.py new file mode 100644 index 0000000..1378da2 --- /dev/null +++ b/octaviaclient/osc/v2/flavor.py @@ -0,0 +1,215 @@ +# 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. + +"""Flavor 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 CreateFlavor(command.ShowOne): + """Create a octavia flavor""" + + def get_parser(self, prog_name): + parser = super(CreateFlavor, self).get_parser(prog_name) + + parser.add_argument( + '--name', + metavar='', + required=True, + help="New flavor name." + ) + parser.add_argument( + '--flavorprofile', + metavar='', + required=True, + help="Flavor profile to add the flavor to (name or ID)." + ) + parser.add_argument( + '--description', + metavar='', + help="Set flavor description." + ) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + default=None, + help="Enable flavor." + ) + admin_group.add_argument( + '--disable', + action='store_true', + default=None, + help="Disable flavor." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.FLAVOR_ROWS + attrs = v2_utils.get_flavor_attrs(self.app.client_manager, + parsed_args) + body = {"flavor": attrs} + data = self.app.client_manager.load_balancer.flavor_create( + json=body) + + formatters = {'flavorprofiles': v2_utils.format_list} + + return (rows, + (utils.get_dict_properties( + data['flavor'], rows, formatters=formatters))) + + +class DeleteFlavor(command.Command): + """Delete a flavor""" + + def get_parser(self, prog_name): + parser = super(DeleteFlavor, self).get_parser(prog_name) + + parser.add_argument( + 'flavor', + metavar='', + help="Flavor to delete (name or ID)" + ) + + return parser + + def take_action(self, parsed_args): + attrs = v2_utils.get_flavor_attrs(self.app.client_manager, + parsed_args) + flavor_id = attrs.pop('flavor_id') + + self.app.client_manager.load_balancer.flavor_delete( + flavor_id=flavor_id) + + +class ListFlavor(lister.Lister): + """List flavor""" + + def get_parser(self, prog_name): + parser = super(ListFlavor, self).get_parser(prog_name) + + parser.add_argument( + '--name', + metavar='', + help="List flavors according to their name." + ) + parser.add_argument( + '--flavorprofile', + metavar='', + help="List flavors according to their flavor profile.", + ) + admin_state_group = parser.add_mutually_exclusive_group() + admin_state_group.add_argument( + '--enable', + action='store_true', + default=None, + help="List enabled flavors." + ) + admin_state_group.add_argument( + '--disable', + action='store_true', + default=None, + help="List disabled flavors." + ) + + return parser + + def take_action(self, parsed_args): + columns = const.FLAVOR_COLUMNS + attrs = v2_utils.get_flavor_attrs(self.app.client_manager, + parsed_args) + data = self.app.client_manager.load_balancer.flavor_list( + **attrs) + formatters = {'flavorprofiles': v2_utils.format_list} + return (columns, + (utils.get_dict_properties(s, columns, formatters=formatters) + for s in data['flavors'])) + + +class ShowFlavor(command.ShowOne): + """Show the details for a single flavor""" + + def get_parser(self, prog_name): + parser = super(ShowFlavor, self).get_parser(prog_name) + + parser.add_argument( + 'flavor', + metavar='', + help="Name or UUID of the flavor." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.FLAVOR_ROWS + attrs = v2_utils.get_flavor_attrs(self.app.client_manager, + parsed_args) + flavor_id = attrs.pop('flavor_id') + + data = self.app.client_manager.load_balancer.flavor_show( + flavor_id=flavor_id + ) + formatters = {'flavorprofiles': v2_utils.format_list} + + return (rows, (utils.get_dict_properties( + data, rows, formatters=formatters))) + + +class SetFlavor(command.Command): + """Update a flavor""" + + def get_parser(self, prog_name): + parser = super(SetFlavor, self).get_parser(prog_name) + + parser.add_argument( + 'flavor', + metavar='', + help='Name or UUID of the flavor to update.' + ) + parser.add_argument( + '--name', + metavar='', + help="Set the name of the flavor." + ) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + default=None, + help="Enable flavor." + ) + admin_group.add_argument( + '--disable', + action='store_true', + default=None, + help="Disable flavor." + ) + + return parser + + def take_action(self, parsed_args): + attrs = v2_utils.get_flavor_attrs(self.app.client_manager, + parsed_args) + flavor_id = attrs.pop('flavor_id') + body = {'flavor': attrs} + + self.app.client_manager.load_balancer.flavor_set( + flavor_id, json=body) diff --git a/octaviaclient/osc/v2/flavorprofile.py b/octaviaclient/osc/v2/flavorprofile.py new file mode 100644 index 0000000..5959048 --- /dev/null +++ b/octaviaclient/osc/v2/flavorprofile.py @@ -0,0 +1,183 @@ +# 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. + +"""Flavor 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 CreateFlavorProfile(command.ShowOne): + """Create a octavia flavor profile""" + + def get_parser(self, prog_name): + parser = super(CreateFlavorProfile, self).get_parser(prog_name) + + parser.add_argument( + '--name', + metavar='', + required=True, + help="New octavia flavor profile name." + ) + parser.add_argument( + '--provider', + metavar='', + required=True, + help="Provider name for the flavor profile." + ) + parser.add_argument( + '--flavor-data', + metavar='', + required=True, + help="The JSON string containing the flavor metadata." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.FLAVORPROFILE_ROWS + attrs = v2_utils.get_flavorprofile_attrs(self.app.client_manager, + parsed_args) + body = {"flavorprofile": attrs} + data = self.app.client_manager.load_balancer.flavorprofile_create( + json=body) + + return (rows, + (utils.get_dict_properties( + data['flavorprofile'], rows, formatters={}))) + + +class DeleteFlavorProfile(command.Command): + """Delete a flavor profile""" + + def get_parser(self, prog_name): + parser = super(DeleteFlavorProfile, self).get_parser(prog_name) + + parser.add_argument( + 'flavorprofile', + metavar='', + help="Flavor profiles to delete (name or ID)" + ) + + return parser + + def take_action(self, parsed_args): + attrs = v2_utils.get_flavorprofile_attrs(self.app.client_manager, + parsed_args) + flavorprofile_id = attrs.pop('flavorprofile_id') + + self.app.client_manager.load_balancer.flavorprofile_delete( + flavorprofile_id=flavorprofile_id) + + +class ListFlavorProfile(lister.Lister): + """List flavor profile""" + + def get_parser(self, prog_name): + parser = super(ListFlavorProfile, self).get_parser(prog_name) + + parser.add_argument( + '--name', + metavar='', + help="List flavor profiles by flavor profile name." + ) + parser.add_argument( + '--provider', + metavar='', + help="List flavor profiles according to their provider.", + ) + + return parser + + def take_action(self, parsed_args): + columns = const.FLAVORPROFILE_COLUMNS + attrs = v2_utils.get_flavorprofile_attrs(self.app.client_manager, + parsed_args) + data = self.app.client_manager.load_balancer.flavorprofile_list( + **attrs) + return (columns, + (utils.get_dict_properties(s, columns, formatters={}) + for s in data['flavorprofiles'])) + + +class ShowFlavorProfile(command.ShowOne): + """Show the details for a single flavor profile""" + + def get_parser(self, prog_name): + parser = super(ShowFlavorProfile, self).get_parser(prog_name) + + parser.add_argument( + 'flavorprofile', + metavar='', + help="Name or UUID of the flavor profile to show." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.FLAVORPROFILE_ROWS + attrs = v2_utils.get_flavorprofile_attrs(self.app.client_manager, + parsed_args) + flavorprofile_id = attrs.pop('flavorprofile_id') + + data = self.app.client_manager.load_balancer.flavorprofile_show( + flavorprofile_id=flavorprofile_id + ) + + return (rows, (utils.get_dict_properties( + data, rows, formatters={}))) + + +class SetFlavorProfile(command.Command): + """Update a flavor profile""" + + def get_parser(self, prog_name): + parser = super(SetFlavorProfile, self).get_parser(prog_name) + + parser.add_argument( + 'flavorprofile', + metavar='', + help='Name or UUID of the flavor profile to update.' + ) + parser.add_argument( + '--name', + metavar='', + help="Set the name of the flavor profile." + ) + parser.add_argument( + '--provider', + metavar='', + help="Set the provider of the flavor profile." + ) + parser.add_argument( + '--flavor-data', + metavar='', + help="Set the flavor data of the flavor profile." + ) + + return parser + + def take_action(self, parsed_args): + attrs = v2_utils.get_flavorprofile_attrs(self.app.client_manager, + parsed_args) + flavorprofile_id = attrs.pop('flavorprofile_id') + body = {'flavorprofile': attrs} + + self.app.client_manager.load_balancer.flavorprofile_set( + flavorprofile_id, json=body) diff --git a/octaviaclient/osc/v2/utils.py b/octaviaclient/osc/v2/utils.py index 010dbfe..e98b337 100644 --- a/octaviaclient/osc/v2/utils.py +++ b/octaviaclient/osc/v2/utils.py @@ -449,6 +449,48 @@ def get_provider_attrs(parsed_args): return _map_attrs(vars(parsed_args), attr_map) +def get_flavor_attrs(client_manager, parsed_args): + attr_map = { + 'name': ('name', str), + 'flavor': ( + 'flavor_id', + 'flavors', + client_manager.load_balancer.flavor_list, + ), + 'flavorprofile': ( + 'flavor_profile_id', + 'flavorprofiles', + client_manager.load_balancer.flavorprofile_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_flavorprofile_attrs(client_manager, parsed_args): + attr_map = { + 'name': ('name', str), + 'flavorprofile': ( + 'flavorprofile_id', + 'flavorprofiles', + client_manager.load_balancer.flavorprofile_list, + ), + 'provider': ('provider_name', str), + 'flavor_data': ('flavor_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 9ef0c8b..39ac4ea 100644 --- a/octaviaclient/tests/unit/api/test_octavia.py +++ b/octaviaclient/tests/unit/api/test_octavia.py @@ -37,6 +37,8 @@ FAKE_HM = uuidutils.generate_uuid() FAKE_PRJ = uuidutils.generate_uuid() FAKE_AMP = uuidutils.generate_uuid() FAKE_PROVIDER = 'fake_provider' +FAKE_FV = uuidutils.generate_uuid() +FAKE_FVPF = uuidutils.generate_uuid() LIST_LB_RESP = { @@ -102,6 +104,16 @@ LIST_PROVIDER_RESP = { {'name': 'provider2', 'description': 'description of provider2'}] } +LIST_FV_RESP = { + 'flavors': [{'name': 'fv1'}, + {'name': 'fv2'}] +} + +LIST_FVPF_RESP = { + 'flavorprofiles': [{'name': 'fvpf1'}, + {'name': 'fvpf2'}] +} + SINGLE_LB_RESP = {'loadbalancer': {'id': FAKE_LB, 'name': 'lb1'}} SINGLE_LB_UPDATE = {"loadbalancer": {"admin_state_up": False}} SINGLE_LB_STATS_RESP = {'bytes_in': '0'} @@ -135,6 +147,12 @@ SINGLE_PROVIDER_CAPABILITY_RESP = { [{'some_capability': 'Capabilicy description'}] } +SINGLE_FV_RESP = {'flavor': {'id': FAKE_FV, 'name': 'fv1'}} +SINGLE_FV_UPDATE = {'flavor': {'enabled': False}} + +SINGLE_FVPF_RESP = {'flavorprofile': {'id': FAKE_FVPF, 'name': 'fvpf1'}} +SINGLE_FVPF_UPDATE = {'flavorprofile': {'provider_name': 'fake_provider'}} + class TestOctaviaClient(utils.TestCase): @@ -933,3 +951,174 @@ class TestLoadBalancer(TestOctaviaClient): ret = self.api.provider_capability_list(FAKE_PROVIDER) self.assertEqual( SINGLE_PROVIDER_CAPABILITY_RESP, ret) + + def test_list_flavor_no_options(self): + self.requests_mock.register_uri( + 'GET', + FAKE_LBAAS_URL + 'flavors', + json=LIST_FV_RESP, + status_code=200, + ) + ret = self.api.flavor_list() + self.assertEqual(LIST_FV_RESP, ret) + + def test_show_flavor(self): + self.requests_mock.register_uri( + 'GET', + FAKE_LBAAS_URL + 'flavors/' + FAKE_FV, + json=SINGLE_FV_RESP, + status_code=200 + ) + ret = self.api.flavor_show(FAKE_FV) + self.assertEqual(SINGLE_FV_RESP['flavor'], ret) + + def test_create_flavor(self): + self.requests_mock.register_uri( + 'POST', + FAKE_LBAAS_URL + 'flavors', + json=SINGLE_FV_RESP, + status_code=200 + ) + ret = self.api.flavor_create(json=SINGLE_FV_RESP) + self.assertEqual(SINGLE_FV_RESP, ret) + + def test_create_flavor_error(self): + self.requests_mock.register_uri( + 'POST', + FAKE_LBAAS_URL + 'flavors', + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(octavia.OctaviaClientException, + self._error_message, + self.api.flavor_create, + json=SINGLE_FV_RESP) + + def test_set_flavor(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_LBAAS_URL + 'flavors/' + FAKE_FV, + json=SINGLE_FV_UPDATE, + status_code=200 + ) + ret = self.api.flavor_set(FAKE_FV, json=SINGLE_FV_UPDATE) + self.assertEqual(SINGLE_FV_UPDATE, ret) + + def test_set_flavor_error(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_LBAAS_URL + 'flavors/' + FAKE_FV, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(octavia.OctaviaClientException, + self._error_message, + self.api.flavor_set, + FAKE_FV, + json=SINGLE_FV_UPDATE) + + def test_delete_flavor(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_LBAAS_URL + 'flavors/' + FAKE_FV, + status_code=200 + ) + ret = self.api.flavor_delete(FAKE_FV) + self.assertEqual(200, ret.status_code) + + def test_delete_flavor_error(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_LBAAS_URL + 'flavors/' + FAKE_FV, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(octavia.OctaviaClientException, + self._error_message, + self.api.flavor_delete, + FAKE_FV) + + def test_list_flavorprofiles_no_options(self): + self.requests_mock.register_uri( + 'GET', + FAKE_LBAAS_URL + 'flavorprofiles', + json=LIST_FVPF_RESP, + status_code=200, + ) + ret = self.api.flavorprofile_list() + self.assertEqual(LIST_FVPF_RESP, ret) + + def test_show_flavorprofile(self): + self.requests_mock.register_uri( + 'GET', + FAKE_LBAAS_URL + 'flavorprofiles/' + FAKE_FVPF, + json=SINGLE_FVPF_RESP, + status_code=200 + ) + ret = self.api.flavorprofile_show(FAKE_FVPF) + self.assertEqual(SINGLE_FVPF_RESP['flavorprofile'], ret) + + def test_create_flavorprofile(self): + self.requests_mock.register_uri( + 'POST', + FAKE_LBAAS_URL + 'flavorprofiles', + json=SINGLE_FVPF_RESP, + status_code=200 + ) + ret = self.api.flavorprofile_create(json=SINGLE_FVPF_RESP) + self.assertEqual(SINGLE_FVPF_RESP, ret) + + def test_create_flavorprofile_error(self): + self.requests_mock.register_uri( + 'POST', + FAKE_LBAAS_URL + 'flavorprofiles', + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(octavia.OctaviaClientException, + self._error_message, + self.api.flavorprofile_create, + json=SINGLE_FVPF_RESP) + + def test_set_flavorprofiles(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_LBAAS_URL + 'flavorprofiles/' + FAKE_FVPF, + json=SINGLE_FVPF_UPDATE, + status_code=200 + ) + ret = self.api.flavorprofile_set(FAKE_FVPF, json=SINGLE_FVPF_UPDATE) + self.assertEqual(SINGLE_FVPF_UPDATE, ret) + + def test_set_flavorprofiles_error(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_LBAAS_URL + 'flavorprofiles/' + FAKE_FVPF, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(octavia.OctaviaClientException, + self._error_message, + self.api.flavorprofile_set, + FAKE_FVPF, json=SINGLE_FVPF_UPDATE) + + def test_delete_flavorprofile(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_LBAAS_URL + 'flavorprofiles/' + FAKE_FVPF, + status_code=200 + ) + ret = self.api.flavorprofile_delete(FAKE_FVPF) + self.assertEqual(200, ret.status_code) + + def test_delete_flavorprofile_error(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_LBAAS_URL + 'flavorprofiles/' + FAKE_FVPF, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(octavia.OctaviaClientException, + self._error_message, + self.api.flavorprofile_delete, + FAKE_FVPF) diff --git a/octaviaclient/tests/unit/osc/v2/constants.py b/octaviaclient/tests/unit/osc/v2/constants.py index 4dac733..eef665f 100644 --- a/octaviaclient/tests/unit/osc/v2/constants.py +++ b/octaviaclient/tests/unit/osc/v2/constants.py @@ -163,3 +163,17 @@ CAPABILITY_ATTRS = { "name": "some_capabilicy", "description": "Description of capabilicy." } + +FLAVOR_ATTRS = { + "id": uuidutils.generate_uuid(), + "name": "fv-name-" + uuidutils.generate_uuid(dashed=True), + "flavor_profile_id": None, + "enabled": True, +} + +FLAVORPROFILE_ATTRS = { + "id": uuidutils.generate_uuid(), + "name": "fvpf-name-" + uuidutils.generate_uuid(dashed=True), + "provider_name": "mock_provider", + "flavor_data": '{"mock_key": "mock_value"}', +} diff --git a/octaviaclient/tests/unit/osc/v2/test_flavor.py b/octaviaclient/tests/unit/osc/v2/test_flavor.py new file mode 100644 index 0000000..ae2ca68 --- /dev/null +++ b/octaviaclient/tests/unit/osc/v2/test_flavor.py @@ -0,0 +1,172 @@ +# 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. + +import copy +import mock + +from osc_lib import exceptions + +from octaviaclient.osc.v2 import constants +from octaviaclient.osc.v2 import flavor +from octaviaclient.tests.unit.osc.v2 import constants as attr_consts +from octaviaclient.tests.unit.osc.v2 import fakes + + +class TestFlavor(fakes.TestOctaviaClient): + + def setUp(self): + super(TestFlavor, self).setUp() + + self._flavor = fakes.createFakeResource('flavor') + self.flavor_info = copy.deepcopy(attr_consts.FLAVOR_ATTRS) + self.columns = copy.deepcopy(constants.FLAVOR_COLUMNS) + + self.api_mock = mock.Mock() + self.api_mock.flavor_list.return_value = copy.deepcopy( + {'flavors': [attr_consts.FLAVOR_ATTRS]}) + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + +class TestFlavorList(TestFlavor): + + def setUp(self): + super(TestFlavorList, self).setUp() + self.datalist = (tuple( + attr_consts.FLAVOR_ATTRS[k] for k in self.columns),) + self.cmd = flavor.ListFlavor(self.app, None) + + def test_flavor_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.flavor_list.assert_called_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_flavor_list_with_options(self): + arglist = ['--name', 'flavor1'] + verifylist = [('name', 'flavor1')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.api_mock.flavor_list.assert_called_with(name='flavor1') + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + +class TestFlavorDelete(TestFlavor): + + def setUp(self): + super(TestFlavorDelete, self).setUp() + self.cmd = flavor.DeleteFlavor(self.app, None) + + def test_flavor_delete(self): + arglist = [self._flavor.id] + verifylist = [ + ('flavor', self._flavor.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.flavor_delete.assert_called_with( + flavor_id=self._flavor.id) + + def test_flavor_delete_failure(self): + arglist = ['unknown_flavor'] + verifylist = [ + ('flavor', 'unknown_flavor') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + self.assertNotCalled(self.api_mock.flavor_delete) + + +class TestFlavorCreate(TestFlavor): + + def setUp(self): + super(TestFlavorCreate, self).setUp() + self.api_mock.flavor_create.return_value = { + 'flavor': self.flavor_info} + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = flavor.CreateFlavor(self.app, None) + + @mock.patch('octaviaclient.osc.v2.utils.get_flavor_attrs') + def test_flavor_create(self, mock_client): + mock_client.return_value = self.flavor_info + arglist = ['--name', self._flavor.name, + '--flavorprofile', 'mock_fvpf_id', + '--description', 'description for flavor'] + verifylist = [ + ('flavorprofile', 'mock_fvpf_id'), + ('name', self._flavor.name), + ('description', 'description for flavor') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.flavor_create.assert_called_with( + json={'flavor': self.flavor_info}) + + +class TestFlavorShow(TestFlavor): + + def setUp(self): + super(TestFlavorShow, self).setUp() + self.api_mock.flavor_show.return_value = self.flavor_info + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = flavor.ShowFlavor(self.app, None) + + def test_flavor_show(self): + arglist = [self._flavor.id] + verifylist = [ + ('flavor', self._flavor.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.flavor_show.assert_called_with( + flavor_id=self._flavor.id) + + +class TestFlavorSet(TestFlavor): + + def setUp(self): + super(TestFlavorSet, self).setUp() + self.cmd = flavor.SetFlavor(self.app, None) + + def test_flavor_set(self): + arglist = [self._flavor.id, '--name', 'new_name'] + verifylist = [ + ('flavor', self._flavor.id), + ('name', 'new_name'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.flavor_set.assert_called_with( + self._flavor.id, json={ + 'flavor': { + 'name': 'new_name' + }}) diff --git a/octaviaclient/tests/unit/osc/v2/test_flavorprofile.py b/octaviaclient/tests/unit/osc/v2/test_flavorprofile.py new file mode 100644 index 0000000..6a752b2 --- /dev/null +++ b/octaviaclient/tests/unit/osc/v2/test_flavorprofile.py @@ -0,0 +1,174 @@ +# 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. + +import copy +import mock + +from osc_lib import exceptions + +from octaviaclient.osc.v2 import constants +from octaviaclient.osc.v2 import flavorprofile +from octaviaclient.tests.unit.osc.v2 import constants as attr_consts +from octaviaclient.tests.unit.osc.v2 import fakes + + +class TestFlavorProfile(fakes.TestOctaviaClient): + + def setUp(self): + super(TestFlavorProfile, self).setUp() + + self._flavorprofile = fakes.createFakeResource('flavorprofile') + self.flavorprofile_info = copy.deepcopy( + attr_consts.FLAVORPROFILE_ATTRS) + self.columns = copy.deepcopy(constants.FLAVORPROFILE_COLUMNS) + + self.api_mock = mock.Mock() + self.api_mock.flavorprofile_list.return_value = copy.deepcopy( + {'flavorprofiles': [attr_consts.FLAVORPROFILE_ATTRS]}) + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + +class TestFlavorProfileList(TestFlavorProfile): + + def setUp(self): + super(TestFlavorProfileList, self).setUp() + self.datalist = (tuple( + attr_consts.FLAVORPROFILE_ATTRS[k] for k in self.columns),) + self.cmd = flavorprofile.ListFlavorProfile(self.app, None) + + def test_flavorprofile_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.flavorprofile_list.assert_called_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_flavorprofile_list_with_options(self): + arglist = ['--name', 'flavorprofile1'] + verifylist = [('name', 'flavorprofile1')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.api_mock.flavorprofile_list.assert_called_with( + name='flavorprofile1') + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + +class TestFlavorProfileDelete(TestFlavorProfile): + + def setUp(self): + super(TestFlavorProfileDelete, self).setUp() + self.cmd = flavorprofile.DeleteFlavorProfile(self.app, None) + + def test_flavorprofile_delete(self): + arglist = [self._flavorprofile.id] + verifylist = [ + ('flavorprofile', self._flavorprofile.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.flavorprofile_delete.assert_called_with( + flavorprofile_id=self._flavorprofile.id) + + def test_flavorprofile_delete_failure(self): + arglist = ['unknown_flavorprofile'] + verifylist = [ + ('flavorprofile', 'unknown_flavorprofile') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + self.assertNotCalled(self.api_mock.flavorprofile_delete) + + +class TestFlavorProfileCreate(TestFlavorProfile): + + def setUp(self): + super(TestFlavorProfileCreate, self).setUp() + self.api_mock.flavorprofile_create.return_value = { + 'flavorprofile': self.flavorprofile_info} + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = flavorprofile.CreateFlavorProfile(self.app, None) + + @mock.patch('octaviaclient.osc.v2.utils.get_flavorprofile_attrs') + def test_flavorprofile_create(self, mock_client): + mock_client.return_value = self.flavorprofile_info + arglist = ['--name', self._flavorprofile.name, + '--provider', 'mock_provider', + '--flavor-data', '{"mock_key": "mock_value"}'] + verifylist = [ + ('provider', 'mock_provider'), + ('name', self._flavorprofile.name), + ('flavor_data', '{"mock_key": "mock_value"}') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.flavorprofile_create.assert_called_with( + json={'flavorprofile': self.flavorprofile_info}) + + +class TestFlavorProfileShow(TestFlavorProfile): + + def setUp(self): + super(TestFlavorProfileShow, self).setUp() + self.api_mock.flavorprofile_show.return_value = self.flavorprofile_info + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = flavorprofile.ShowFlavorProfile(self.app, None) + + def test_flavorprofile_show(self): + arglist = [self._flavorprofile.id] + verifylist = [ + ('flavorprofile', self._flavorprofile.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.flavorprofile_show.assert_called_with( + flavorprofile_id=self._flavorprofile.id) + + +class TestFlavorProfileSet(TestFlavorProfile): + + def setUp(self): + super(TestFlavorProfileSet, self).setUp() + self.cmd = flavorprofile.SetFlavorProfile(self.app, None) + + def test_flavorprofile_set(self): + arglist = [self._flavorprofile.id, '--name', 'new_name'] + verifylist = [ + ('flavorprofile', self._flavorprofile.id), + ('name', 'new_name'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.flavorprofile_set.assert_called_with( + self._flavorprofile.id, json={ + 'flavorprofile': { + 'name': 'new_name' + }}) diff --git a/releasenotes/notes/add-flavor-support-75c6d5bec48b1d18.yaml b/releasenotes/notes/add-flavor-support-75c6d5bec48b1d18.yaml new file mode 100644 index 0000000..deb5db6 --- /dev/null +++ b/releasenotes/notes/add-flavor-support-75c6d5bec48b1d18.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds client support for octavia provider flavor and flavor_profile. diff --git a/setup.cfg b/setup.cfg index 21158f5..40b7016 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,6 +77,16 @@ openstack.load_balancer.v2 = 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_flavorprofile_create = octaviaclient.osc.v2.flavorprofile:CreateFlavorProfile + loadbalancer_flavorprofile_list = octaviaclient.osc.v2.flavorprofile:ListFlavorProfile + loadbalancer_flavorprofile_delete = octaviaclient.osc.v2.flavorprofile:DeleteFlavorProfile + loadbalancer_flavorprofile_show = octaviaclient.osc.v2.flavorprofile:ShowFlavorProfile + loadbalancer_flavorprofile_set = octaviaclient.osc.v2.flavorprofile:SetFlavorProfile + loadbalancer_flavor_create = octaviaclient.osc.v2.flavor:CreateFlavor + loadbalancer_flavor_list = octaviaclient.osc.v2.flavor:ListFlavor + loadbalancer_flavor_delete = octaviaclient.osc.v2.flavor:DeleteFlavor + loadbalancer_flavor_show = octaviaclient.osc.v2.flavor:ShowFlavor + loadbalancer_flavor_set = octaviaclient.osc.v2.flavor:SetFlavor [build_sphinx] source-dir = doc/source