diff --git a/gbpclient/gbp/v2_0/servicechain.py b/gbpclient/gbp/v2_0/servicechain.py index d832628..63fca55 100644 --- a/gbpclient/gbp/v2_0/servicechain.py +++ b/gbpclient/gbp/v2_0/servicechain.py @@ -147,6 +147,107 @@ class DeleteServiceChainInstance(neutronV20.DeleteCommand): log = logging.getLogger(__name__ + '.DeleteServiceChainInstance') +class ListServiceProfile(neutronV20.ListCommand): + """List service profiles that belong to a given tenant.""" + + resource = 'service_profile' + log = logging.getLogger(__name__ + '.ListServiceProfile') + _formatters = {} + list_columns = ['id', 'name', 'description', 'service_type'] + pagination_support = True + sorting_support = True + + +class ShowServiceProfile(neutronV20.ShowCommand): + """Show information of a given service profile.""" + + resource = 'service_profile' + log = logging.getLogger(__name__ + '.ShowServiceProfile') + + +class CreateServiceProfile(neutronV20.CreateCommand): + """Create a service profile.""" + + resource = 'service_profile' + log = logging.getLogger(__name__ + '.CreateServiceProfile') + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', + help=_('Name for the Service Profile.')) + parser.add_argument( + '--description', + help=_('Description of the Service Profile.')) + parser.add_argument( + '--shared', type=bool, + help=_('Shared flag')) + parser.add_argument( + '--vendor', + help=_('Vendor providing the service node')) + parser.add_argument( + '--insertion-mode', + help=_('Insertion mode of the service')) + parser.add_argument( + '--servicetype', dest='service_type', + help=_('Type of the service')) + parser.add_argument( + '--service-flavor', + help=_('Flavor of the service')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'description', 'tenant_id', 'shared', + 'vendor', 'insertion_mode', 'service_type', + 'service_flavor']) + return body + + +class UpdateServiceProfile(neutronV20.UpdateCommand): + """Update a given service profile.""" + + resource = 'service_profile' + log = logging.getLogger(__name__ + '.UpdateServiceProfile') + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Name for the Service Profile.')) + parser.add_argument( + '--description', + help=_('Description of the Service Profile.')) + parser.add_argument( + '--shared', type=bool, + help=_('Shared flag')) + parser.add_argument( + '--vendor', + help=_('Vendor providing the service node')) + parser.add_argument( + '--insertion-mode', + help=_('Insertion mode of the service')) + parser.add_argument( + '--servicetype', dest='service_type', + help=_('Type of the service')) + parser.add_argument( + '--service-flavor', + help=_('Flavor of the service')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'description', 'shared', 'vendor', + 'insertion_mode', 'service_type', + 'service_flavor']) + return body + + +class DeleteServiceProfile(neutronV20.DeleteCommand): + """Delete a given service profile.""" + + resource = 'service_profile' + log = logging.getLogger(__name__ + '.DeleteServiceProfile') + + class ListServiceChainNode(neutronV20.ListCommand): """List service chain nodes that belong to a given tenant.""" @@ -181,9 +282,15 @@ class CreateServiceChainNode(neutronV20.CreateCommand): parser.add_argument( '--servicetype', dest='service_type', help=_('Service type ID or the Service Type name')) + parser.add_argument( + '--service-profile', + help=_('Service Profile name or UUID')) parser.add_argument( '--config', help=_('Service Configuration for the Service Chain Node.')) + parser.add_argument( + '--shared', type=bool, + help=_('Shared flag')) parser.add_argument( '--template-file', help=_('Service Configuration Template for the Service Chain ' @@ -195,6 +302,11 @@ class CreateServiceChainNode(neutronV20.CreateCommand): def args2body(self, parsed_args): body = {self.resource: {}, } + if parsed_args.service_profile: + body[self.resource]['service_profile_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'service_profile', + parsed_args.service_profile) if parsed_args.template_file: if os.path.isfile(parsed_args.template_file): tpl_files, template = template_utils.get_template_contents( @@ -205,7 +317,7 @@ class CreateServiceChainNode(neutronV20.CreateCommand): "Please check the path" % parsed_args.template_file) neutronV20.update_dict(parsed_args, body[self.resource], - ['name', 'service_type', 'config', + ['name', 'service_type', 'config', 'shared', 'tenant_id', 'param_names', 'description']) return body @@ -220,15 +332,26 @@ class UpdateServiceChainNode(neutronV20.UpdateCommand): parser.add_argument( '--servicetype', dest='service_type', help=_('Service type ID or the Service Type name')) + parser.add_argument( + '--service-profile', + help=_('Service Profile name or UUID')) parser.add_argument( '--config', help=_('Service Configuration for the Service Chain Node.')) + parser.add_argument( + '--shared', type=bool, + help=_('Shared flag')) def args2body(self, parsed_args): body = {self.resource: {}, } + if parsed_args.service_profile: + body[self.resource]['service_profile_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'service_profile', + parsed_args.service_profile) neutronV20.update_dict(parsed_args, body[self.resource], - ['name', 'service_type', 'config', - 'tenant_id', 'description']) + ['name', 'service_type', 'config', 'shared', + 'description']) return body @@ -273,6 +396,9 @@ class CreateServiceChainSpec(neutronV20.CreateCommand): parser.add_argument( '--nodes', metavar='NODES', type=string.split, help=_('Service Chain Node ID or name of the Service Chain Node')) + parser.add_argument( + '--shared', type=bool, + help=_('Shared flag')) def args2body(self, parsed_args): body = {self.resource: {}, } @@ -285,7 +411,7 @@ class CreateServiceChainSpec(neutronV20.CreateCommand): elem) for elem in parsed_args.nodes] neutronV20.update_dict(parsed_args, body[self.resource], - ['name', 'tenant_id', 'description']) + ['name', 'tenant_id', 'description', 'shared']) return body @@ -300,6 +426,9 @@ class UpdateServiceChainSpec(neutronV20.UpdateCommand): '--nodes', type=string.split, help=_('List of Service Chain Node IDs or names of the Service ' 'Chain Nodes')) + parser.add_argument( + '--shared', type=bool, + help=_('Shared flag')) def args2body(self, parsed_args): body = {self.resource: {}, } @@ -309,6 +438,8 @@ class UpdateServiceChainSpec(neutronV20.UpdateCommand): self.get_client(), 'servicechain_node', elem) for elem in parsed_args.nodes] + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'description', 'shared']) return body diff --git a/gbpclient/gbpshell.py b/gbpclient/gbpshell.py index d5a335e..ba84f7c 100644 --- a/gbpclient/gbpshell.py +++ b/gbpclient/gbpshell.py @@ -152,6 +152,11 @@ COMMAND_V2 = { 'policy-rule-set-update': gbp.UpdatePolicyRuleSet, 'policy-rule-set-list': gbp.ListPolicyRuleSet, 'policy-rule-set-show': gbp.ShowPolicyRuleSet, + 'service-profile-list': servicechain.ListServiceProfile, + 'service-profile-show': servicechain.ShowServiceProfile, + 'service-profile-create': servicechain.CreateServiceProfile, + 'service-profile-delete': servicechain.DeleteServiceProfile, + 'service-profile-update': servicechain.UpdateServiceProfile, 'servicechain-node-list': servicechain.ListServiceChainNode, 'servicechain-node-show': servicechain.ShowServiceChainNode, 'servicechain-node-create': servicechain.CreateServiceChainNode, diff --git a/gbpclient/tests/unit/test_cli20_service_profile.py b/gbpclient/tests/unit/test_cli20_service_profile.py new file mode 100644 index 0000000..540c02d --- /dev/null +++ b/gbpclient/tests/unit/test_cli20_service_profile.py @@ -0,0 +1,170 @@ +# Copyright 2015 OpenStack Foundation. +# 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 sys + +from gbpclient.gbp.v2_0 import servicechain +from gbpclient.tests.unit import test_cli20 + + +class CLITestV20ServiceProfileJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20ServiceProfileJSON, self).setUp() + + def test_create_service_profile_with_mandatory_params(self): + """service-profile-create with all mandatory params.""" + resource = 'service_profile' + cmd = servicechain.CreateServiceProfile(test_cli20.MyApp(sys.stdout), + None) + name = 'my-name' + tenant_id = 'my-tenant' + my_id = 'my-id' + args = ['--tenant-id', tenant_id, name] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, my_id, args, + position_names, position_values, + tenant_id=tenant_id) + + def test_create_service_profile_with_all_params(self): + """service-profile-create with all params.""" + resource = 'service_profile' + cmd = servicechain.CreateServiceProfile(test_cli20.MyApp(sys.stdout), + None) + name = 'my-name' + description = 'My Service Profile' + tenant_id = 'my-tenant' + shared = 'True' + vendor = 'vendor' + insertion_mode = 'some mode' + service_type = 'servicetype1' + service_flavor = 'cherry-garcia' + my_id = 'my-id' + args = ['--description', description, + '--tenant-id', tenant_id, + '--shared', shared, + '--vendor', vendor, + '--insertion-mode', insertion_mode, + '--servicetype', service_type, + '--service-flavor', service_flavor, + name] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, my_id, args, + position_names, position_values, + description=description, + tenant_id=tenant_id, + shared=True, + vendor=vendor, + insertion_mode=insertion_mode, + service_type=service_type, + service_flavor=service_flavor) + + def test_list_service_profiles(self): + """service-profile-list.""" + resources = 'service_profiles' + cmd = servicechain.ListServiceProfile(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources(resources, cmd, True) + + def test_list_service_profiles_pagination(self): + """service-profile-list.""" + resources = 'service_profiles' + cmd = servicechain.ListServiceProfile(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_service_profiles_sort(self): + """service-profile-list --sort-key name --sort-key id --sort-key asc + --sort-key desc + """ + resources = 'service_profiles' + cmd = servicechain.ListServiceProfile(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_service_profiles_limit(self): + """service-profile-list -P.""" + resources = 'service_profiles' + cmd = servicechain.ListServiceProfile(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_service_profile_id(self): + """service-profile-show test_id.""" + resource = 'service_profile' + cmd = servicechain.ShowServiceProfile(test_cli20.MyApp(sys.stdout), + None) + args = ['--fields', 'id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, args, ['id']) + + def test_show_service_profile_id_name(self): + """service-profile-show.""" + resource = 'service_profile' + cmd = servicechain.ShowServiceProfile(test_cli20.MyApp(sys.stdout), + None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id', 'name']) + + def test_update_service_profile(self): + """service-profile-update myid --name myname --tags a b.""" + resource = 'service_profile' + cmd = servicechain.UpdateServiceProfile(test_cli20.MyApp(sys.stdout), + None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname', + '--tags', 'a', 'b'], + {'name': 'myname', 'tags': ['a', 'b'], }) + + def test_update_service_profile_with_all_params(self): + resource = 'service_profile' + cmd = servicechain.UpdateServiceProfile(test_cli20.MyApp(sys.stdout), + None) + name = 'new-name' + description = 'My Updated Service Profile' + shared = 'True' + vendor = 'open-source' + insertion_mode = 'another mode' + service_type = 'servicetype2' + service_flavor = 'phish-food' + body = { + 'name': name, + 'description': description, + 'shared': True, + 'vendor': vendor, + 'insertion_mode': insertion_mode, + 'service_type': service_type, + 'service_flavor': service_flavor} + args = ['myid', '--name', name, + '--description', description, + '--shared', shared, + '--vendor', vendor, + '--insertion-mode', insertion_mode, + '--servicetype', service_type, + '--service-flavor', service_flavor] + self._test_update_resource(resource, cmd, 'myid', args, body) + + def test_delete_service_profile(self): + """service-profile-delete my-id.""" + resource = 'service_profile' + cmd = servicechain.DeleteServiceProfile(test_cli20.MyApp(sys.stdout), + None) + my_id = 'my-id' + args = [my_id] + self._test_delete_resource(resource, cmd, my_id, args) diff --git a/gbpclient/tests/unit/test_cli20_servicechain_node.py b/gbpclient/tests/unit/test_cli20_servicechain_node.py index b10dda6..7b85f75 100644 --- a/gbpclient/tests/unit/test_cli20_servicechain_node.py +++ b/gbpclient/tests/unit/test_cli20_servicechain_node.py @@ -49,11 +49,15 @@ class CLITestV20ServiceChainNodeJSON(test_cli20.CLITestV20Base): config = 'config1' tenant_id = 'my-tenant' description = 'My Service Chain Node' + service_profile_id = 'my-service-profile' my_id = 'my-id' + shared = 'True' args = ['--servicetype', service_type, '--config', config, '--tenant-id', tenant_id, '--description', description, + '--service-profile', service_profile_id, + '--shared', shared, name] position_names = ['name', ] position_values = [name, ] @@ -61,7 +65,9 @@ class CLITestV20ServiceChainNodeJSON(test_cli20.CLITestV20Base): position_names, position_values, service_type=service_type, config=config, tenant_id=tenant_id, - description=description) + description=description, + service_profile_id=service_profile_id, + shared=True) def test_list_servicechain_nodes(self): """service-chain-node-list.""" @@ -122,6 +128,26 @@ class CLITestV20ServiceChainNodeJSON(test_cli20.CLITestV20Base): '--tags', 'a', 'b'], {'name': 'myname', 'tags': ['a', 'b'], }) + def test_update_servicechain_node_with_all_params(self): + resource = 'servicechain_node' + cmd = servicechain.UpdateServiceChainNode(test_cli20.MyApp(sys.stdout), + None) + body = { + 'name': 'new_name', + 'description': 'new_description', + 'service_profile_id': 'new_service_profile_id', + 'shared': True, + } + args = ['myid', '--name', 'new_name', + '--description', 'new_description', + '--service-profile', 'new_service_profile_id', + '--shared', 'True'] + self._test_update_resource(resource, cmd, 'myid', args, body) + + # REVISIT(rkukura): Not sure why the following two methods are + # needed, since allow_put for both the service_type and config + # attributes is False. + def test_update_servicechain_node_with_servicetype(self): resource = 'servicechain_node' cmd = servicechain.UpdateServiceChainNode(test_cli20.MyApp(sys.stdout), diff --git a/gbpclient/tests/unit/test_cli20_servicechain_spec.py b/gbpclient/tests/unit/test_cli20_servicechain_spec.py index f08f05a..fc697e8 100644 --- a/gbpclient/tests/unit/test_cli20_servicechain_spec.py +++ b/gbpclient/tests/unit/test_cli20_servicechain_spec.py @@ -50,16 +50,18 @@ class CLITestV20ServiceChainSpecJSON(test_cli20.CLITestV20Base): tenant_id = 'my-tenant' description = 'My Service Chain Spec' my_id = 'my-id' + shared = 'True' args = ['--nodes', nodes_arg, '--tenant-id', tenant_id, '--description', description, + '--shared', shared, name] position_names = ['name', ] position_values = [name, ] self._test_create_resource(resource, cmd, name, my_id, args, position_names, position_values, nodes=nodes_res, tenant_id=tenant_id, - description=description) + description=description, shared=True) def test_list_servicechain_specs(self): """service-chain-spec-list.""" @@ -120,18 +122,22 @@ class CLITestV20ServiceChainSpecJSON(test_cli20.CLITestV20Base): '--tags', 'a', 'b'], {'name': 'myname', 'tags': ['a', 'b'], }) - def test_update_servicechain_spec_with_nodes(self): + def test_update_servicechain_node_with_all_params(self): resource = 'servicechain_spec' cmd = servicechain.UpdateServiceChainSpec(test_cli20.MyApp(sys.stdout), None) nodes_arg = 'node1 node2' nodes_res = ['node1', 'node2'] - description = 'My Service Chain Spec' body = { + 'name': 'new_name', + 'description': 'new_description', 'nodes': nodes_res, - 'description': description + 'shared': True, } - args = ['myid', '--nodes', nodes_arg, '--description', description] + args = ['myid', '--name', 'new_name', + '--description', 'new_description', + '--nodes', nodes_arg, + '--shared', 'True'] self._test_update_resource(resource, cmd, 'myid', args, body) def test_delete_servicechain_spec(self): diff --git a/gbpclient/v2_0/client.py b/gbpclient/v2_0/client.py index b258d73..34c68ec 100644 --- a/gbpclient/v2_0/client.py +++ b/gbpclient/v2_0/client.py @@ -171,6 +171,8 @@ class Client(object): policy_rule_path = "/grouppolicy/policy_rules/%s" policy_rule_sets_path = "/grouppolicy/policy_rule_sets" policy_rule_set_path = "/grouppolicy/policy_rule_sets/%s" + service_profiles_path = "/servicechain/service_profiles" + service_profile_path = "/servicechain/service_profiles/%s" servicechain_nodes_path = "/servicechain/servicechain_nodes" servicechain_node_path = "/servicechain/servicechain_nodes/%s" servicechain_specs_path = "/servicechain/servicechain_specs" @@ -574,6 +576,38 @@ class Client(object): """Deletes the specified Policy Rule Set.""" return self.delete(self.policy_rule_set_path % (policy_rule_set)) + @APIParamsCall + def list_service_profiles(self, retrieve_all=True, **_params): + + """Fetches a list of all service profiles for a tenant.""" + # Pass filters in "params" argument to do_request + + return self.list('service_profiles', self.service_profiles_path, + retrieve_all, **_params) + + @APIParamsCall + def show_service_profile(self, service_profile, **_params): + """Fetches information of a certain service profile.""" + return self.get(self.service_profile_path % (service_profile), + params=_params) + + @APIParamsCall + def create_service_profile(self, body=None): + """Creates a new service profile.""" + return self.post(self.service_profiles_path, body=body) + + @APIParamsCall + def update_service_profile(self, service_profile, body=None): + """Updates a service profile.""" + return self.put(self.service_profile_path % (service_profile), + body=body) + + @APIParamsCall + def delete_service_profile(self, service_profile): + """Deletes the specified service profile.""" + return self.delete(self.service_profile_path % (service_profile)) + + @APIParamsCall def list_servicechain_nodes(self, retrieve_all=True, **_params): """Fetches a list of all service chain nodes for a tenant.""" diff --git a/test-requirements.txt b/test-requirements.txt index c430f14..974f01b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -e git://github.com/openstack/python-neutronclient.git#egg=python-neutronclient -hacking>=0.8.0,<0.9 +hacking>=0.9.2,<0.10 cliff-tablib>=1.0 coverage>=3.6 diff --git a/tox.ini b/tox.ini index 756f013..b9b9998 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,9 @@ downloadcache = ~/cache/pip [flake8] # E125 continuation line does not distinguish itself from next logical line # H302 import only modules -ignore = E125,H302 +# +# REVISIT: Fix code and remove E129,E251,H305,H307,H405,H904 added for +# hacking 0.9.2 +ignore = E125,H302,E129,E251,H305,H307,H405,H904 show-source = true exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools