diff --git a/.gitignore b/.gitignore index 1bbb8b1..9987ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ gbp/vcsversion.py gbpclient/versioninfo run_tests.err.log run_tests.log +subunit.log .autogenerated .coverage .testrepository/ diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..b8b3ff9 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,26 @@ +Neutron Style Commandments +================================ + +- Step 1: Read the OpenStack Style Commandments + http://docs.openstack.org/developer/hacking/ +- Step 2: Read on + + +Running Tests +------------- +The testing system is based on a combination of tox and testr. The canonical +approach to running tests is to simply run the command `tox`. This will +create virtual environments, populate them with depenedencies and run all of +the tests that OpenStack CI systems run. Behind the scenes, tox is running +`testr run --parallel`, but is set up such that you can supply any additional +testr arguments that are needed to tox. For example, you can run: +`tox -- --analyze-isolation` to cause tox to tell testr to add +--analyze-isolation to its argument list. + +It is also possible to run the tests inside of a virtual environment +you have created, or it is possible that you have all of the dependencies +installed locally already. In this case, you can interact with the testr +command directly. Running `testr run` will run the entire test suite. `testr +run --parallel` will run it in parallel (this is the default incantation tox +uses.) More information about testr can be found at: +http://wiki.openstack.org/testr diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/gbpclient/gbp/__init__.py b/gbpclient/gbp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gbpclient/gbp/v2_0/__init__.py b/gbpclient/gbp/v2_0/__init__.py new file mode 100644 index 0000000..9be1d1d --- /dev/null +++ b/gbpclient/gbp/v2_0/__init__.py @@ -0,0 +1,3 @@ +from neutronclient.neutron import v2_0 as neutronV2_0 + +_get_resource_plural = neutronV2_0._get_resource_plural diff --git a/gbpclient/gbp/v2_0/groupbasedpolicy.py b/gbpclient/gbp/v2_0/groupbasedpolicy.py new file mode 100644 index 0000000..9da824e --- /dev/null +++ b/gbpclient/gbp/v2_0/groupbasedpolicy.py @@ -0,0 +1,861 @@ +# 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 logging +import string + +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.openstack.common.gettextutils import _ +from oslo.serialization import jsonutils + + +def _format_network_service_params(net_svc_policy): + try: + return '\n'.join([jsonutils.dumps(param) for param in + net_svc_policy['network_service_params']]) + except (TypeError, KeyError): + return '' + + +class ListEndpoint(neutronV20.ListCommand): + """List policy_targets that belong to a given tenant.""" + + resource = 'endpoint' + log = logging.getLogger(__name__ + '.ListEndpoint') + _formatters = {} + list_columns = ['id', 'name', 'description', 'endpoint_group_id'] + pagination_support = True + sorting_support = True + + +class ShowEndpoint(neutronV20.ShowCommand): + """Show information of a given policy_target.""" + + resource = 'endpoint' + log = logging.getLogger(__name__ + '.ShowEndpoint') + + +class CreateEndpoint(neutronV20.CreateCommand): + """Create a policy_target for a given tenant.""" + + resource = 'endpoint' + log = logging.getLogger(__name__ + '.CreateEndpoint') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the policy_target')) + parser.add_argument( + '--endpoint-group', metavar='EPG', + default='', + help=_('group uuid')) + parser.add_argument( + '--port', + default='', + help=_('Neutron Port')) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of policy_target to create')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description']) + if parsed_args.endpoint_group: + body[self.resource]['endpoint_group_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'endpoint_group', + parsed_args.endpoint_group) + if parsed_args.port: + body[self.resource]['port_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'port', + parsed_args.port) + return body + + +class DeleteEndpoint(neutronV20.DeleteCommand): + """Delete a given policy_target.""" + + resource = 'endpoint' + log = logging.getLogger(__name__ + '.DeleteEndpoint') + + +class UpdateEndpoint(neutronV20.UpdateCommand): + """Update policy_target's information.""" + + resource = 'endpoint' + log = logging.getLogger(__name__ + '.UpdateEndpoint') + + +class ListEndpointGroup(neutronV20.ListCommand): + """List groups that belong to a given tenant.""" + + resource = 'endpoint_group' + log = logging.getLogger(__name__ + '.ListEndpointGroup') + list_columns = ['id', 'name', 'description'] + pagination_support = True + sorting_support = True + + +class ShowEndpointGroup(neutronV20.ShowCommand): + """Show information of a given group.""" + + resource = 'endpoint_group' + log = logging.getLogger(__name__ + '.ShowEndpointGroup') + + +class CreateEndpointGroup(neutronV20.CreateCommand): + """Create a group for a given tenant.""" + + resource = 'endpoint_group' + log = logging.getLogger(__name__ + '.CreateEndpointGroup') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the group')) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of group to create')) + parser.add_argument( + '--l2-policy', metavar='L2_POLICY', + default='', + help=_('L2 policy uuid')) + parser.add_argument( + '--provided-contracts', type=utils.str2dict, + default={}, + help=_('Dictionary of provided contract uuids')) + parser.add_argument( + '--consumed-contracts', type=utils.str2dict, + default={}, + help=_('Dictionary of consumed contract uuids')) + parser.add_argument( + '--network-service-policy', metavar='NETWORK_SERVICE_POLICY', + default='', + help=_('Network service policy uuid')) + parser.add_argument( + '--subnets', type=string.split, + help=_('Subnet to map the group')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + if parsed_args.l2_policy: + body[self.resource]['l2_policy_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'l2_policy', + parsed_args.l2_policy) + + if parsed_args.network_service_policy: + body[self.resource]['network_service_policy_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'network_service_policy', + parsed_args.network_service_policy) + + if parsed_args.provided_contracts: + for key in parsed_args.provided_contracts.keys(): + id_key = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'contract', + key) + parsed_args.provided_contracts[id_key] = \ + parsed_args.provided_contracts.pop(key) + + if parsed_args.consumed_contracts: + for key in parsed_args.consumed_contracts.keys(): + id_key = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'contract', + key) + parsed_args.consumed_contracts[id_key] = \ + parsed_args.consumed_contracts.pop(key) + + if parsed_args.subnets: + for subnet in parsed_args.subnets: + subnet_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', + subnet) + parsed_args.subnets.remove(subnet) + parsed_args.subnets.append(subnet_id) + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description', + 'provided_contracts', 'subnets', + 'consumed_contracts']) + + return body + + +class DeleteEndpointGroup(neutronV20.DeleteCommand): + """Delete a given group.""" + + resource = 'endpoint_group' + log = logging.getLogger(__name__ + '.DeleteEndpointGroup') + + +class UpdateEndpointGroup(neutronV20.UpdateCommand): + """Update group's information.""" + + resource = 'endpoint_group' + log = logging.getLogger(__name__ + '.UpdateEndpointGroup') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the group')) + parser.add_argument( + '--l2-policy', metavar='L2_POLICY', + help=_('L2 policy uuid')) + parser.add_argument( + '--network-service-policy', metavar='NETWORK_SERVICE_POLICY', + help=_('Network Service Policy uuid')) + parser.add_argument( + '--provided-contracts', type=utils.str2dict, + help=_('Dictionary of provided contract uuids')) + parser.add_argument( + '--consumed-contracts', type=utils.str2dict, + help=_('Dictionary of consumed contract uuids')) + parser.add_argument( + '--subnets', type=string.split, + help=_('Subnet to map the group')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + if parsed_args.l2_policy: + body[self.resource]['l2_policy_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'l2_policy', + parsed_args.l2_policy) + + if parsed_args.network_service_policy: + body[self.resource]['network_service_policy_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'network_service_policy', + parsed_args.l2_policy) + + if parsed_args.provided_contracts: + for key in parsed_args.provided_contracts.keys(): + id_key = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'contract', + key) + parsed_args.provided_contracts[id_key] = \ + parsed_args.provided_contracts.pop(key) + + if parsed_args.consumed_contracts: + for key in parsed_args.consumed_contracts.keys(): + id_key = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'contract', + key) + parsed_args.consumed_contracts[id_key] = \ + parsed_args.consumed_contracts.pop(key) + + if parsed_args.subnets: + for subnet in parsed_args.subnets: + subnet_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', + subnet) + parsed_args.subnets.remove(subnet) + parsed_args.subnets.append(subnet_id) + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description', + 'provided_contracts', 'subnets', + 'consumed_contracts']) + + return body + + +class ListL2Policy(neutronV20.ListCommand): + """List L2 Policies that belong to a given tenant.""" + + resource = 'l2_policy' + log = logging.getLogger(__name__ + '.ListL2Policy') + _formatters = {} + list_columns = ['id', 'name', 'description', 'l3_policy_id'] + pagination_support = True + sorting_support = True + + +class ShowL2Policy(neutronV20.ShowCommand): + """Show information of a given l2_policy.""" + + resource = 'l2_policy' + log = logging.getLogger(__name__ + '.ShowL2Policy') + + +class CreateL2Policy(neutronV20.CreateCommand): + """Create a bridge_domain for a given tenant.""" + + resource = 'l2_policy' + log = logging.getLogger(__name__ + '.CreateL2Policy') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the l2_policy')) + parser.add_argument( + '--network', + help=_('Network to map the l2_policy')) + parser.add_argument( + '--l3-policy', + default='', + help=_('l3_policy uuid')) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of l2_policy to create')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description']) + if parsed_args.l3_policy: + body[self.resource]['l3_policy_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'l3_policy', + parsed_args.l3_policy) + if parsed_args.network: + body[self.resource]['network_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'network', + parsed_args.network) + return body + + +class DeleteL2Policy(neutronV20.DeleteCommand): + """Delete a given l2_policy.""" + + resource = 'l2_policy' + log = logging.getLogger(__name__ + '.DeleteL2Policy') + + +class UpdateL2Policy(neutronV20.UpdateCommand): + """Update l2_policy's information.""" + + resource = 'l2_policy' + log = logging.getLogger(__name__ + '.UpdateL2Policy') + + +class ListL3Policy(neutronV20.ListCommand): + """List l3_policies that belong to a given tenant.""" + + resource = 'l3_policy' + log = logging.getLogger(__name__ + '.ListL3Policy') + _formatters = {} + list_columns = ['id', 'name', 'description', 'ip_pool', + 'subnet_prefix_length'] + pagination_support = True + sorting_support = True + + +class ShowL3Policy(neutronV20.ShowCommand): + """Show information of a given l3_policy.""" + + resource = 'l3_policy' + log = logging.getLogger(__name__ + '.ShowL3Policy') + + +class CreateL3Policy(neutronV20.CreateCommand): + """Create a l3_policy for a given tenant.""" + + resource = 'l3_policy' + log = logging.getLogger(__name__ + '.CreateL3Policy') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the l3_policy')) + parser.add_argument( + '--ip-version', + type=int, + default=4, choices=[4, 6], + help=_('IP version, default is 4')) + parser.add_argument( + '--ip-pool', + help=_('CIDR of IP pool to create, default is 10.0.0.0/8')) + parser.add_argument( + '--subnet-prefix-length', + type=int, + default=24, + help=_('Subnet prefix length, default is 24')) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of l3_policy to create')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description', + 'ip_version', 'ip_pool', + 'subnet_prefix_length']) + + return body + + +class DeleteL3Policy(neutronV20.DeleteCommand): + """Delete a given l3_policy.""" + + resource = 'l3_policy' + log = logging.getLogger(__name__ + '.DeleteL3Policy') + + +class UpdateL3Policy(neutronV20.UpdateCommand): + """Update l3_policy's information.""" + + resource = 'l3_policy' + log = logging.getLogger(__name__ + '.UpdateL3Policy') + + +class ListNetworkServicePolicy(neutronV20.ListCommand): + """List Network Service Policies that belong to a given tenant.""" + + resource = 'network_service_policy' + log = logging.getLogger(__name__ + '.ListNetworkServicePolicy') + _formatters = {'network_servie_params': _format_network_service_params} + list_columns = ['id', 'name', 'description', 'network_service_params'] + pagination_support = True + sorting_support = True + + +class ShowNetworkServicePolicy(neutronV20.ShowCommand): + """Show information of a given network_service_policy.""" + + resource = 'network_service_policy' + log = logging.getLogger(__name__ + '.ShowNetworkServicePolicy') + + +class CreateNetworkServicePolicy(neutronV20.CreateCommand): + """Create a Network Service Policy for a given tenant.""" + + resource = 'network_service_policy' + log = logging.getLogger(__name__ + '.CreateNetworkServicePolicy') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the network_service_policy')) + parser.add_argument( + 'name', + help=_('Name of network_service_policy to create')) + parser.add_argument( + '--network-service-params', + metavar='type=PARAM_TYPE,name=PARAM_NAME,value=PARAM_VALUE', + action='append', dest='network_service_params', + type=utils.str2dict, + help=_('Network service params for this network service policy' + '(This option can be repeated).')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + """ + if parsed_args.name: + body[self.resource].update({'name': parsed_args.name}) + + if parsed_args.description: + body[self.resource].update({'description': parsed_args.name}) + + if parsed_args.network_service_params: + body[self.resource]['network_service_params'] = ( + parsed_args.network_sercice_params) + """ + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description', + 'network_service_params']) + return body + + +class DeleteNetworkServicePolicy(neutronV20.DeleteCommand): + """Delete a given network_service_policy.""" + + resource = 'network_service_policy' + log = logging.getLogger(__name__ + '.DeleteNetworkServicePolicy') + + +class UpdateNetworkServicePolicy(neutronV20.UpdateCommand): + """Update network_service_policy's information.""" + + resource = 'network_service_policy' + log = logging.getLogger(__name__ + '.UpdateNetworkServicePolicy') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the network_service_policy')) + parser.add_argument( + '--name', + help=_('Name of network_service_policy to create')) + parser.add_argument( + '--network-service-params', + metavar='type=PARAM_TYPE,name=PARAM_NAME,value=PARAM_VALUE', + action='append', dest='network_service_params', + type=utils.str2dict, + help=_('Network service params for this network service policy' + '(This option can be repeated).')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description', + 'network_service_params']) + return body + + +class ListPolicyClassifier(neutronV20.ListCommand): + """List classifiers that belong to a given tenant.""" + + resource = 'policy_classifier' + log = logging.getLogger(__name__ + '.ListPolicyClassifier') + _formatters = {} + list_columns = ['id', 'name', 'protocol', 'port_range', 'direction'] + pagination_support = True + sorting_support = True + + +class ShowPolicyClassifier(neutronV20.ShowCommand): + """Show information of a given classifier.""" + + resource = 'policy_classifier' + log = logging.getLogger(__name__ + '.ShowPolicyClassifier') + + +class CreatePolicyClassifier(neutronV20.CreateCommand): + """Create a classifier for a given tenant.""" + + resource = 'policy_classifier' + log = logging.getLogger(__name__ + '.CreatePolicyClassifier') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the policy classifier')) + parser.add_argument( + '--protocol', + choices=['tcp', 'udp', 'icmp'], + help=_('Protocol')) + parser.add_argument( + '--port-range', + help=_('Port range')) + parser.add_argument( + '--direction', + choices=['in', 'out', 'bi', ''], + help=_('Direction')) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of classifier to create')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description', + 'protocol', 'port_range', 'direction']) + + return body + + +class DeletePolicyClassifier(neutronV20.DeleteCommand): + """Delete a given classifier.""" + + resource = 'policy_classifier' + log = logging.getLogger(__name__ + '.DeletePolicyClassifier') + + +class UpdatePolicyClassifier(neutronV20.UpdateCommand): + """Update classifier's information.""" + + resource = 'policy_classifier' + log = logging.getLogger(__name__ + '.UpdatePolicyClassifier') + + +class ListPolicyAction(neutronV20.ListCommand): + """List actions that belong to a given tenant.""" + + resource = 'policy_action' + log = logging.getLogger(__name__ + '.ListPolicyAction') + _formatters = {} + list_columns = ['id', 'name', 'action_type', 'action_value'] + pagination_support = True + sorting_support = True + + +class ShowPolicyAction(neutronV20.ShowCommand): + """Show information of a given action.""" + + resource = 'policy_action' + log = logging.getLogger(__name__ + '.ShowPolicyAction') + + +class CreatePolicyAction(neutronV20.CreateCommand): + """Create a action for a given tenant.""" + + resource = 'policy_action' + log = logging.getLogger(__name__ + '.CreatePolicyAction') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the policy action')) + parser.add_argument( + '--action-type', + help=_('Type of action')) + parser.add_argument( + '--action-value', + help=_('uuid of service for redirect action')) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of action to create')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description', + 'action_type', 'action_value']) + + return body + + +class DeletePolicyAction(neutronV20.DeleteCommand): + """Delete a given action.""" + + resource = 'policy_action' + log = logging.getLogger(__name__ + '.DeletePolicyAction') + + +class UpdatePolicyAction(neutronV20.UpdateCommand): + """Update action's information.""" + + resource = 'policy_action' + log = logging.getLogger(__name__ + '.UpdatePolicyAction') + + +class ListPolicyRule(neutronV20.ListCommand): + """List policy_rules that belong to a given tenant.""" + + resource = 'policy_rule' + log = logging.getLogger(__name__ + '.ListPolicyRule') + _formatters = {} + list_columns = ['id', 'name', 'enabled', 'classifier_id', + 'actions'] + pagination_support = True + sorting_support = True + + +class ShowPolicyRule(neutronV20.ShowCommand): + """Show information of a given policy_rule.""" + + resource = 'policy_rule' + log = logging.getLogger(__name__ + '.ShowPolicyRule') + + +class CreatePolicyRule(neutronV20.CreateCommand): + """Create a policy_rule for a given tenant.""" + + resource = 'policy_rule' + log = logging.getLogger(__name__ + '.CreatePolicyRule') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the policy_rule')) + parser.add_argument( + '--enabled', type=bool, + help=_('Enable flag')) + parser.add_argument( + '--classifier', + help=_('uuid of policy classifier')) + parser.add_argument( + '--actions', type=string.split, + help=_('List of policy actions')) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of policy_rule to create')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + if parsed_args.actions: + body[self.resource]['policy_actions'] = [ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), + 'policy_action', + elem) for elem in parsed_args.actions] + + if parsed_args.classifier: + body[self.resource]['policy_classifier_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), + 'policy_classifier', + parsed_args.classifier) + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description', + 'enabled']) + + return body + + +class DeletePolicyRule(neutronV20.DeleteCommand): + """Delete a given policy_rule.""" + + resource = 'policy_rule' + log = logging.getLogger(__name__ + '.DeletePolicyRule') + + +class UpdatePolicyRule(neutronV20.UpdateCommand): + """Update policy_rule's information.""" + + resource = 'policy_rule' + log = logging.getLogger(__name__ + '.UpdatePolicyRule') + + def add_known_arguments(self, parser): + parser.add_argument( + '--enabled', type=bool, + help=_('Enable flag')) + parser.add_argument( + '--classifier', + help=_('uuid of policy classifier')) + parser.add_argument( + '--actions', type=string.split, + help=_('List of policy actions')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + if parsed_args.actions: + body[self.resource]['policy_actions'] = [ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), + 'policy_action', + elem) for elem in parsed_args.actions] + + if parsed_args.classifier: + body[self.resource]['policy_classifier_id'] = \ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), + 'policy_classifier', + parsed_args.classifier) + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'description', + 'enabled']) + return body + + +class ListContract(neutronV20.ListCommand): + """List contracts that belong to a given tenant.""" + + resource = 'contract' + log = logging.getLogger(__name__ + '.ListContract') + _formatters = {} + list_columns = ['id', 'name', 'ploicy_rules'] + pagination_support = True + sorting_support = True + + +class ShowContract(neutronV20.ShowCommand): + """Show information of a given contract.""" + + resource = 'contract' + log = logging.getLogger(__name__ + '.ShowContract') + + +class CreateContract(neutronV20.CreateCommand): + """Create a contract for a given tenant.""" + + resource = 'contract' + log = logging.getLogger(__name__ + '.CreateContract') + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of the contract')) + parser.add_argument( + '--policy-rules', type=string.split, + help=_('List of policy rules')) + parser.add_argument( + '--child-contracts', type=string.split, + help=_('List of child contracts')) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of contract to create')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + + if parsed_args.policy_rules: + body[self.resource]['policy_rules'] = [ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), + 'policy_rule', + elem) for elem in parsed_args.policy_rules] + + if parsed_args.child_contracts: + body[self.resource]['child_contracts'] = [ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), + 'contract', + elem) for elem in parsed_args.child_contracts] + + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'tenant_id', 'description']) + return body + + +class DeleteContract(neutronV20.DeleteCommand): + """Delete a given contract.""" + + resource = 'contract' + log = logging.getLogger(__name__ + '.DeleteContract') + + +class UpdateContract(neutronV20.UpdateCommand): + """Update contract's information.""" + + resource = 'contract' + log = logging.getLogger(__name__ + '.UpdateContract') + + def add_known_arguments(self, parser): + parser.add_argument( + '--policy-rules', type=string.split, + help=_('List of policy rules')) + parser.add_argument( + '--child-contracts', type=string.split, + help=_('List of child contracts')) + + def args2body(self, parsed_args): + body = {self.resource: {}, } + if parsed_args.policy_rules: + body[self.resource]['policy_rules'] = [ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), + 'policy_rule', + elem) for elem in parsed_args.policy_rules] + parsed_args.policy_rules = body[self.resource]['policy_rules'] + + if parsed_args.child_contracts: + body[self.resource]['child_contracts'] = [ + neutronV20.find_resourceid_by_name_or_id( + self.get_client(), + 'contract', + elem) for elem in parsed_args.child_contracts] + parsed_args.child_contracts = parsed_args.child_contracts + neutronV20.update_dict(parsed_args, body[self.resource], + ['name', 'description', 'policy_rules', + 'child_contracts']) + return body diff --git a/gbpclient/gbpshell.py b/gbpclient/gbpshell.py new file mode 100644 index 0000000..64c733e --- /dev/null +++ b/gbpclient/gbpshell.py @@ -0,0 +1,793 @@ +# 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. +# + +""" +Command-line interface to the GBP APIs +""" + +from __future__ import print_function + +import argparse +import logging +import os +import sys + +from keystoneclient.auth.identity import v2 as v2_auth +from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient import discover +from keystoneclient.openstack.common.apiclient import exceptions as ks_exc +from keystoneclient import session +import six.moves.urllib.parse as urlparse + +from cliff import app +from cliff import commandmanager +from neutronclient.common import clientmanager +from neutronclient.common import exceptions as exc +from neutronclient.common import utils +from neutronclient.openstack.common.gettextutils import _ +from neutronclient.openstack.common import strutils +from neutronclient.version import __version__ + +from gbpclient.gbp.v2_0 import groupbasedpolicy as gbp + +VERSION = '2.0' +NEUTRON_API_VERSION = '2.0' +clientmanager.neutron_client.API_VERSIONS = { + '2.0': 'gbpclient.v2_0.client.Client', +} + + +def run_command(cmd, cmd_parser, sub_argv): + _argv = sub_argv + index = -1 + values_specs = [] + if '--' in sub_argv: + index = sub_argv.index('--') + _argv = sub_argv[:index] + values_specs = sub_argv[index:] + known_args, _values_specs = cmd_parser.parse_known_args(_argv) + cmd.values_specs = (index == -1 and _values_specs or values_specs) + return cmd.run(known_args) + + +def env(*_vars, **kwargs): + """Search for the first defined of possibly many env vars. + + Returns the first environment variable defined in vars, or + returns the default defined in kwargs. + + """ + for v in _vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') + + +def check_non_negative_int(value): + try: + value = int(value) + except ValueError: + raise argparse.ArgumentTypeError(_("invalid int value: %r") % value) + if value < 0: + raise argparse.ArgumentTypeError(_("input value %d is negative") % + value) + return value + + +COMMAND_V2 = { + 'policy-target-create': gbp.CreateEndpoint, + 'policy-target-delete': gbp.DeleteEndpoint, + 'policy-target-update': gbp.UpdateEndpoint, + 'policy-target-list': gbp.ListEndpoint, + 'policy-target-show': gbp.ShowEndpoint, + 'group-create': gbp.CreateEndpointGroup, + 'group-delete': gbp.DeleteEndpointGroup, + 'group-update': gbp.UpdateEndpointGroup, + 'group-list': gbp.ListEndpointGroup, + 'group-show': gbp.ShowEndpointGroup, + 'l2policy-create': gbp.CreateL2Policy, + 'l2policy-delete': gbp.DeleteL2Policy, + 'l2policy-update': gbp.UpdateL2Policy, + 'l2policy-list': gbp.ListL2Policy, + 'l2policy-show': gbp.ShowL2Policy, + 'l3policy-create': gbp.CreateL3Policy, + 'l3policy-delete': gbp.DeleteL3Policy, + 'l3policy-update': gbp.UpdateL3Policy, + 'l3policy-list': gbp.ListL3Policy, + 'l3policy-show': gbp.ShowL3Policy, + 'network-service-policy-create': gbp.CreateNetworkServicePolicy, + 'network-service-policy-delete': gbp.DeleteNetworkServicePolicy, + 'network-service-policy-update': gbp.UpdateNetworkServicePolicy, + 'network-service-policy-list': gbp.ListNetworkServicePolicy, + 'network-service-policy-show': gbp.ShowNetworkServicePolicy, + 'policy-classifier-create': gbp.CreatePolicyClassifier, + 'policy-classifier-delete': gbp.DeletePolicyClassifier, + 'policy-classifier-update': gbp.UpdatePolicyClassifier, + 'policy-classifier-list': gbp.ListPolicyClassifier, + 'policy-classifier-show': gbp.ShowPolicyClassifier, + 'policy-action-create': gbp.CreatePolicyAction, + 'policy-action-delete': gbp.DeletePolicyAction, + 'policy-action-update': gbp.UpdatePolicyAction, + 'policy-action-list': gbp.ListPolicyAction, + 'policy-action-show': gbp.ShowPolicyAction, + 'policy-rule-create': gbp.CreatePolicyRule, + 'policy-rule-delete': gbp.DeletePolicyRule, + 'policy-rule-update': gbp.UpdatePolicyRule, + 'policy-rule-list': gbp.ListPolicyRule, + 'policy-rule-show': gbp.ShowPolicyRule, + 'policy-rule-set-create': gbp.CreateContract, + 'policy-rule-set-delete': gbp.DeleteContract, + 'policy-rule-set-update': gbp.UpdateContract, + 'policy-rule-set-list': gbp.ListContract, + 'policy-rule-set-show': gbp.ShowContract, +} + +COMMANDS = {'2.0': COMMAND_V2} + + +class HelpAction(argparse.Action): + """Provide a custom action so the -h and --help options + to the main app will print a list of the commands. + + The commands are determined by checking the CommandManager + instance, passed in as the "default" value for the action. + """ + def __call__(self, parser, namespace, values, option_string=None): + outputs = [] + max_len = 0 + app = self.default + parser.print_help(app.stdout) + app.api_version = '2.0' # Check this + app.stdout.write(_('\nCommands for GBP API v%s:\n') % app.api_version) + command_manager = app.command_manager + for name, ep in sorted(command_manager): + factory = ep.load() + cmd = factory(self, None) + one_liner = cmd.get_description().split('\n')[0] + outputs.append((name, one_liner)) + max_len = max(len(name), max_len) + for (name, one_liner) in outputs: + app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner)) + sys.exit(0) + + +class GBPShell(app.App): + + # verbose logging levels + WARNING_LEVEL = 0 + INFO_LEVEL = 1 + DEBUG_LEVEL = 2 + CONSOLE_MESSAGE_FORMAT = '%(message)s' + DEBUG_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' + log = logging.getLogger(__name__) + + def __init__(self, apiversion): + super(GBPShell, self).__init__( + description=__doc__.strip(), + version=VERSION, + command_manager=commandmanager.CommandManager('gbp.cli'), ) + self.commands = COMMANDS + for k, v in self.commands[apiversion].items(): + self.command_manager.add_command(k, v) + + # This is instantiated in initialize_app() only when using + # password flow auth + self.auth_client = None + self.api_version = apiversion + + def build_option_parser(self, description, version): + """Return an argparse option parser for this application. + + Subclasses may override this method to extend + the parser with more global options. + + :param description: full description of the application + :paramtype description: str + :param version: version number for the application + :paramtype version: str + """ + parser = argparse.ArgumentParser( + description=description, + add_help=False, ) + parser.add_argument( + '--version', + action='version', + version=__version__, ) + parser.add_argument( + '-v', '--verbose', '--debug', + action='count', + dest='verbose_level', + default=self.DEFAULT_VERBOSE_LEVEL, + help=_('Increase verbosity of output and show tracebacks on' + ' errors. You can repeat this option.')) + parser.add_argument( + '-q', '--quiet', + action='store_const', + dest='verbose_level', + const=0, + help=_('Suppress output except warnings and errors.')) + parser.add_argument( + '-h', '--help', + action=HelpAction, + nargs=0, + default=self, # tricky + help=_("Show this help message and exit.")) + parser.add_argument( + '-r', '--retries', + metavar="NUM", + type=check_non_negative_int, + default=0, + help=_("How many times the request to the Neutron server should " + "be retried if it fails.")) + # FIXME(bklei): this method should come from python-keystoneclient + self._append_global_identity_args(parser) + + return parser + + def _append_global_identity_args(self, parser): + # FIXME(bklei): these are global identity (Keystone) arguments which + # should be consistent and shared by all service clients. Therefore, + # they should be provided by python-keystoneclient. We will need to + # refactor this code once this functionality is available in + # python-keystoneclient. + # + # Note: At that time we'll need to decide if we can just abandon + # the deprecated args (--service-type and --endpoint-type). + + parser.add_argument( + '--os-service-type', metavar='', + default=env('OS_NETWORK_SERVICE_TYPE', default='network'), + help=_('Defaults to env[OS_NETWORK_SERVICE_TYPE] or network.')) + + parser.add_argument( + '--os-endpoint-type', metavar='', + default=env('OS_ENDPOINT_TYPE', default='publicURL'), + help=_('Defaults to env[OS_ENDPOINT_TYPE] or publicURL.')) + + # FIXME(bklei): --service-type is deprecated but kept in for + # backward compatibility. + parser.add_argument( + '--service-type', metavar='', + default=env('OS_NETWORK_SERVICE_TYPE', default='network'), + help=_('DEPRECATED! Use --os-service-type.')) + + # FIXME(bklei): --endpoint-type is deprecated but kept in for + # backward compatibility. + parser.add_argument( + '--endpoint-type', metavar='', + default=env('OS_ENDPOINT_TYPE', default='publicURL'), + help=_('DEPRECATED! Use --os-endpoint-type.')) + + parser.add_argument( + '--os-auth-strategy', metavar='', + default=env('OS_AUTH_STRATEGY', default='keystone'), + help=_('DEPRECATED! Only keystone is supported.')) + + parser.add_argument( + '--os_auth_strategy', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-auth-url', metavar='', + default=env('OS_AUTH_URL'), + help=_('Authentication URL, defaults to env[OS_AUTH_URL].')) + parser.add_argument( + '--os_auth_url', + help=argparse.SUPPRESS) + + project_name_group = parser.add_mutually_exclusive_group() + project_name_group.add_argument( + '--os-tenant-name', metavar='', + default=env('OS_TENANT_NAME'), + help=_('Authentication tenant name, defaults to ' + 'env[OS_TENANT_NAME].')) + project_name_group.add_argument( + '--os-project-name', + metavar='', + default=utils.env('OS_PROJECT_NAME'), + help='Another way to specify tenant name. ' + 'This option is mutually exclusive with ' + ' --os-tenant-name. ' + 'Defaults to env[OS_PROJECT_NAME].') + + parser.add_argument( + '--os_tenant_name', + help=argparse.SUPPRESS) + + project_id_group = parser.add_mutually_exclusive_group() + project_id_group.add_argument( + '--os-tenant-id', metavar='', + default=env('OS_TENANT_ID'), + help=_('Authentication tenant ID, defaults to ' + 'env[OS_TENANT_ID].')) + project_id_group.add_argument( + '--os-project-id', + metavar='', + default=utils.env('OS_PROJECT_ID'), + help='Another way to specify tenant ID. ' + 'This option is mutually exclusive with ' + ' --os-tenant-id. ' + 'Defaults to env[OS_PROJECT_ID].') + + parser.add_argument( + '--os-username', metavar='', + default=utils.env('OS_USERNAME'), + help=_('Authentication username, defaults to env[OS_USERNAME].')) + parser.add_argument( + '--os_username', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-user-id', metavar='', + default=env('OS_USER_ID'), + help=_('Authentication user ID (Env: OS_USER_ID)')) + + parser.add_argument( + '--os_user_id', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-user-domain-id', + metavar='', + default=utils.env('OS_USER_DOMAIN_ID'), + help='OpenStack user domain ID. ' + 'Defaults to env[OS_USER_DOMAIN_ID].') + + parser.add_argument( + '--os_user_domain_id', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-user-domain-name', + metavar='', + default=utils.env('OS_USER_DOMAIN_NAME'), + help='OpenStack user domain name. ' + 'Defaults to env[OS_USER_DOMAIN_NAME].') + + parser.add_argument( + '--os_user_domain_name', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os_project_id', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os_project_name', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-project-domain-id', + metavar='', + default=utils.env('OS_PROJECT_DOMAIN_ID'), + help='Defaults to env[OS_PROJECT_DOMAIN_ID].') + + parser.add_argument( + '--os-project-domain-name', + metavar='', + default=utils.env('OS_PROJECT_DOMAIN_NAME'), + help='Defaults to env[OS_PROJECT_DOMAIN_NAME].') + + parser.add_argument( + '--os-cert', + metavar='', + default=utils.env('OS_CERT'), + help=_("Path of certificate file to use in SSL " + "connection. This file can optionally be " + "prepended with the private key. Defaults " + "to env[OS_CERT]")) + + parser.add_argument( + '--os-cacert', + metavar='', + default=env('OS_CACERT', default=None), + help=_("Specify a CA bundle file to use in " + "verifying a TLS (https) server certificate. " + "Defaults to env[OS_CACERT]")) + + parser.add_argument( + '--os-key', + metavar='', + default=utils.env('OS_KEY'), + help=_("Path of client key to use in SSL " + "connection. This option is not necessary " + "if your key is prepended to your certificate " + "file. Defaults to env[OS_KEY]")) + + parser.add_argument( + '--os-password', metavar='', + default=utils.env('OS_PASSWORD'), + help=_('Authentication password, defaults to env[OS_PASSWORD].')) + parser.add_argument( + '--os_password', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-region-name', metavar='', + default=env('OS_REGION_NAME'), + help=_('Authentication region name, defaults to ' + 'env[OS_REGION_NAME].')) + parser.add_argument( + '--os_region_name', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-token', metavar='', + default=env('OS_TOKEN'), + help=_('Authentication token, defaults to env[OS_TOKEN].')) + parser.add_argument( + '--os_token', + help=argparse.SUPPRESS) + + parser.add_argument( + '--http-timeout', metavar='', + default=env('OS_NETWORK_TIMEOUT', default=None), type=float, + help=_('Timeout in seconds to wait for an HTTP response. Defaults ' + 'to env[OS_NETWORK_TIMEOUT] or None if not specified.')) + + parser.add_argument( + '--os-url', metavar='', + default=env('OS_URL'), + help=_('Defaults to env[OS_URL].')) + parser.add_argument( + '--os_url', + help=argparse.SUPPRESS) + + parser.add_argument( + '--insecure', + action='store_true', + default=env('NEUTRONCLIENT_INSECURE', default=False), + help=_("Explicitly allow neutronclient to perform \"insecure\" " + "SSL (https) requests. The server's certificate will " + "not be verified against any certificate authorities. " + "This option should be used with caution.")) + + def _bash_completion(self): + """Prints all of the commands and options for bash-completion.""" + commands = set() + options = set() + for option, _action in self.parser._option_string_actions.items(): + options.add(option) + for command_name, command in self.command_manager: + commands.add(command_name) + cmd_factory = command.load() + cmd = cmd_factory(self, None) + cmd_parser = cmd.get_parser('') + for option, _action in cmd_parser._option_string_actions.items(): + options.add(option) + print(' '.join(commands | options)) + + def run(self, argv): + """Equivalent to the main program for the application. + + :param argv: input arguments and options + :paramtype argv: list of str + """ + try: + index = 0 + command_pos = -1 + help_pos = -1 + help_command_pos = -1 + for arg in argv: + if arg == 'bash-completion': + self._bash_completion() + return 0 + if arg in self.commands[self.api_version]: + if command_pos == -1: + command_pos = index + elif arg in ('-h', '--help'): + if help_pos == -1: + help_pos = index + elif arg == 'help': + if help_command_pos == -1: + help_command_pos = index + index = index + 1 + if command_pos > -1 and help_pos > command_pos: + argv = ['help', argv[command_pos]] + if help_command_pos > -1 and command_pos == -1: + argv[help_command_pos] = '--help' + self.options, remainder = self.parser.parse_known_args(argv) + self.configure_logging() + self.interactive_mode = not remainder + self.initialize_app(remainder) + except Exception as err: + if self.options.verbose_level >= self.DEBUG_LEVEL: + self.log.exception(unicode(err)) + raise + else: + self.log.error(unicode(err)) + return 1 + result = 1 + if self.interactive_mode: + _argv = [sys.argv[0]] + sys.argv = _argv + result = self.interact() + else: + result = self.run_subcommand(remainder) + return result + + def run_subcommand(self, argv): + subcommand = self.command_manager.find_command(argv) + cmd_factory, cmd_name, sub_argv = subcommand + cmd = cmd_factory(self, self.options) + err = None + result = 1 + try: + self.prepare_to_run_command(cmd) + full_name = (cmd_name + if self.interactive_mode + else ' '.join([self.NAME, cmd_name]) + ) + cmd_parser = cmd.get_parser(full_name) + return run_command(cmd, cmd_parser, sub_argv) + except Exception as err: + if self.options.verbose_level >= self.DEBUG_LEVEL: + self.log.exception(unicode(err)) + else: + self.log.error(unicode(err)) + try: + self.clean_up(cmd, result, err) + except Exception as err2: + if self.options.verbose_level >= self.DEBUG_LEVEL: + self.log.exception(unicode(err2)) + else: + self.log.error(_('Could not clean up: %s'), unicode(err2)) + if self.options.verbose_level >= self.DEBUG_LEVEL: + raise + else: + try: + self.clean_up(cmd, result, None) + except Exception as err3: + if self.options.verbose_level >= self.DEBUG_LEVEL: + self.log.exception(unicode(err3)) + else: + self.log.error(_('Could not clean up: %s'), unicode(err3)) + return result + + def authenticate_user(self): + """Make sure the user has provided all of the authentication + info we need. + """ + if self.options.os_auth_strategy == 'keystone': + if self.options.os_token or self.options.os_url: + # Token flow auth takes priority + if not self.options.os_token: + raise exc.CommandError( + _("You must provide a token via" + " either --os-token or env[OS_TOKEN]")) + + if not self.options.os_url: + raise exc.CommandError( + _("You must provide a service URL via" + " either --os-url or env[OS_URL]")) + + else: + # Validate password flow auth + project_info = (self.options.os_tenant_name or + self.options.os_tenant_id or + (self.options.os_project_name and + (self.options.project_domain_name or + self.options.project_domain_id)) or + self.options.os_project_id) + + if (not self.options.os_username + and not self.options.os_user_id): + raise exc.CommandError( + _("You must provide a username or user ID via" + " --os-username, env[OS_USERNAME] or" + " --os-user_id, env[OS_USER_ID]")) + + if not self.options.os_password: + raise exc.CommandError( + _("You must provide a password via" + " either --os-password or env[OS_PASSWORD]")) + + if (not project_info): + # tenent is deprecated in Keystone v3. Use the latest + # terminology instead. + raise exc.CommandError( + _("You must provide a project_id or project_name (" + "with project_domain_name or project_domain_id) " + "via " + " --os-project-id (env[OS_PROJECT_ID])" + " --os-project-name (env[OS_PROJECT_NAME])," + " --os-project-domain-id " + "(env[OS_PROJECT_DOMAIN_ID])" + " --os-project-domain-name " + "(env[OS_PROJECT_DOMAIN_NAME])")) + + if not self.options.os_auth_url: + raise exc.CommandError( + _("You must provide an auth url via" + " either --os-auth-url or via env[OS_AUTH_URL]")) + else: # not keystone + if not self.options.os_url: + raise exc.CommandError( + _("You must provide a service URL via" + " either --os-url or env[OS_URL]")) + + auth_session = self._get_keystone_session() + + self.client_manager = clientmanager.ClientManager( + token=self.options.os_token, + url=self.options.os_url, + auth_url=self.options.os_auth_url, + tenant_name=self.options.os_tenant_name, + tenant_id=self.options.os_tenant_id, + username=self.options.os_username, + user_id=self.options.os_user_id, + password=self.options.os_password, + region_name=self.options.os_region_name, + api_version=self.api_version, + auth_strategy=self.options.os_auth_strategy, + # FIXME (bklei) honor deprecated service_type and + # endpoint type until they are removed + service_type=self.options.os_service_type or + self.options.service_type, + endpoint_type=self.options.os_endpoint_type or self.endpoint_type, + insecure=self.options.insecure, + ca_cert=self.options.os_cacert, + timeout=self.options.http_timeout, + retries=self.options.retries, + raise_errors=False, + session=auth_session, + auth=auth_session.auth, + log_credentials=True) + return + + def initialize_app(self, argv): + """Global app init bits: + + * set up API versions + * validate authentication info + """ + + super(GBPShell, self).initialize_app(argv) + + self.api_version = {'network': self.api_version} + + # If the user is not asking for help, make sure they + # have given us auth. + cmd_name = None + if argv: + cmd_info = self.command_manager.find_command(argv) + cmd_factory, cmd_name, sub_argv = cmd_info + if self.interactive_mode or cmd_name != 'help': + self.authenticate_user() + + def clean_up(self, cmd, result, err): + self.log.debug('clean_up %s', cmd.__class__.__name__) + if err: + self.log.debug('Got an error: %s', unicode(err)) + + def configure_logging(self): + """Create logging handlers for any log output.""" + root_logger = logging.getLogger('') + + # Set up logging to a file + root_logger.setLevel(logging.DEBUG) + + # Send higher-level messages to the console via stderr + console = logging.StreamHandler(self.stderr) + console_level = {self.WARNING_LEVEL: logging.WARNING, + self.INFO_LEVEL: logging.INFO, + self.DEBUG_LEVEL: logging.DEBUG, + }.get(self.options.verbose_level, logging.DEBUG) + console.setLevel(console_level) + if logging.DEBUG == console_level: + formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT) + else: + formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT) + logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING) + console.setFormatter(formatter) + root_logger.addHandler(console) + return + + def get_v2_auth(self, v2_auth_url): + return v2_auth.Password( + v2_auth_url, + username=self.options.os_username, + password=self.options.os_password, + tenant_id=self.options.os_tenant_id, + tenant_name=self.options.os_tenant_name) + + def get_v3_auth(self, v3_auth_url): + project_id = self.options.os_project_id or self.options.os_tenant_id + project_name = (self.options.os_project_name or + self.options.os_tenant_name) + + return v3_auth.Password( + v3_auth_url, + username=self.options.os_username, + password=self.options.os_password, + user_id=self.options.os_user_id, + user_domain_name=self.options.os_user_domain_name, + user_domain_id=self.options.os_user_domain_id, + project_id=project_id, + project_name=project_name, + project_domain_name=self.options.os_project_domain_name, + project_domain_id=self.options.os_project_domain_id + ) + + def _discover_auth_versions(self, session, auth_url): + # discover the API versions the server is supporting base on the + # given URL + try: + ks_discover = discover.Discover(session=session, auth_url=auth_url) + return (ks_discover.url_for('2.0'), ks_discover.url_for('3.0')) + except ks_exc.ClientException: + # Identity service may not support discover API version. + # Lets try to figure out the API version from the original URL. + url_parts = urlparse.urlparse(auth_url) + (scheme, netloc, path, params, query, fragment) = url_parts + path = path.lower() + if path.startswith('/v3'): + return (None, auth_url) + elif path.startswith('/v2'): + return (auth_url, None) + else: + # not enough information to determine the auth version + msg = _('Unable to determine the Keystone version ' + 'to authenticate with using the given ' + 'auth_url. Identity service may not support API ' + 'version discovery. Please provide a versioned ' + 'auth_url instead.') + raise exc.CommandError(msg) + + def _get_keystone_session(self): + # first create a Keystone session + cacert = self.options.os_cacert or None + cert = self.options.os_cert or None + key = self.options.os_key or None + insecure = self.options.insecure or False + ks_session = session.Session.construct(dict(cacert=cacert, + cert=cert, + key=key, + insecure=insecure)) + # discover the supported keystone versions using the given url + (v2_auth_url, v3_auth_url) = self._discover_auth_versions( + session=ks_session, + auth_url=self.options.os_auth_url) + + # Determine which authentication plugin to use. First inspect the + # auth_url to see the supported version. If both v3 and v2 are + # supported, then use the highest version if possible. + user_domain_name = self.options.os_user_domain_name or None + user_domain_id = self.options.os_user_domain_id or None + project_domain_name = self.options.os_project_domain_name or None + project_domain_id = self.options.os_project_domain_id or None + domain_info = (user_domain_name or user_domain_id or + project_domain_name or project_domain_id) + + if (v2_auth_url and not domain_info) or not v3_auth_url: + ks_session.auth = self.get_v2_auth(v2_auth_url) + else: + ks_session.auth = self.get_v3_auth(v3_auth_url) + + return ks_session + + +def main(argv=sys.argv[1:]): + try: + return GBPShell(NEUTRON_API_VERSION).run(map(strutils.safe_decode, + argv)) + except exc.NeutronClientException: + return 1 + except Exception as e: + print(unicode(e)) + return 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/gbpclient/tests/unit/test_auth.py b/gbpclient/tests/unit/test_auth.py index 5561ecc..6e00b62 100644 --- a/gbpclient/tests/unit/test_auth.py +++ b/gbpclient/tests/unit/test_auth.py @@ -30,7 +30,6 @@ from keystoneclient import exceptions as ks_exceptions from keystoneclient.fixture import v2 as ks_v2_fixture from keystoneclient.fixture import v3 as ks_v3_fixture from keystoneclient import session - from neutronclient import client from neutronclient.common import exceptions from neutronclient.common import utils diff --git a/gbpclient/tests/unit/test_cli20.py b/gbpclient/tests/unit/test_cli20.py index 38b5aa1..8a160cf 100644 --- a/gbpclient/tests/unit/test_cli20.py +++ b/gbpclient/tests/unit/test_cli20.py @@ -1,6 +1,3 @@ -# Copyright 2012 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 @@ -14,198 +11,56 @@ # under the License. # -import contextlib -import itertools -import sys -import urllib - -import fixtures from mox3 import mox -from oslotest import base -import requests -import six -import six.moves.urllib.parse as urlparse - -from neutronclient.common import constants from neutronclient.common import exceptions -from neutronclient.neutron import v2_0 as neutronV2_0 -from neutronclient import shell -from neutronclient.v2_0 import client +from neutronclient.tests.unit import test_cli20 as neutron_test_cli20 +import requests -API_VERSION = "2.0" -FORMAT = 'json' -TOKEN = 'testtoken' -ENDURL = 'localurl' +from gbpclient.gbp import v2_0 as gbpV2_0 +from gbpclient import gbpshell +from gbpclient.v2_0 import client as gbpclient + +API_VERSION = neutron_test_cli20.API_VERSION +FORMAT = neutron_test_cli20.FORMAT +TOKEN = neutron_test_cli20.TOKEN +ENDURL = neutron_test_cli20.ENDURL +capture_std_streams = neutron_test_cli20.capture_std_streams +end_url = neutron_test_cli20.end_url -@contextlib.contextmanager -def capture_std_streams(): - fake_stdout, fake_stderr = six.StringIO(), six.StringIO() - stdout, stderr = sys.stdout, sys.stderr - try: - sys.stdout, sys.stderr = fake_stdout, fake_stderr - yield fake_stdout, fake_stderr - finally: - sys.stdout, sys.stderr = stdout, stderr +class FakeStdout(neutron_test_cli20.FakeStdout): + + pass -class FakeStdout: +class MyResp(neutron_test_cli20.MyResp): - def __init__(self): - self.content = [] - - def write(self, text): - self.content.append(text) - - def make_string(self): - result = '' - for line in self.content: - result = result + line - return result + pass -class MyResp(object): - def __init__(self, status_code, headers=None, reason=None): - self.status_code = status_code - self.headers = headers or {} - self.reason = reason +class MyApp(neutron_test_cli20.MyApp): + + pass -class MyApp(object): - def __init__(self, _stdout): - self.stdout = _stdout +class MyUrlComparator(neutron_test_cli20.MyUrlComparator): + + pass -def end_url(path, query=None, format=FORMAT): - _url_str = ENDURL + "/v" + API_VERSION + path + "." + format - return query and _url_str + "?" + query or _url_str +class MyComparator(neutron_test_cli20.MyComparator): + + pass -class MyUrlComparator(mox.Comparator): - def __init__(self, lhs, client): - self.lhs = lhs - self.client = client +class CLITestV20Base(neutron_test_cli20.CLITestV20Base): - def equals(self, rhs): - lhsp = urlparse.urlparse(self.lhs) - rhsp = urlparse.urlparse(rhs) - - return (lhsp.scheme == rhsp.scheme and - lhsp.netloc == rhsp.netloc and - lhsp.path == rhsp.path and - urlparse.parse_qs(lhsp.query) == urlparse.parse_qs(rhsp.query)) - - def __str__(self): - if self.client and self.client.format != FORMAT: - lhs_parts = self.lhs.split("?", 1) - if len(lhs_parts) == 2: - lhs = ("%s.%s?%s" % (lhs_parts[0][:-4], - self.client.format, - lhs_parts[1])) - else: - lhs = ("%s.%s" % (lhs_parts[0][:-4], - self.client.format)) - return lhs - return self.lhs - - def __repr__(self): - return str(self) - - -class MyComparator(mox.Comparator): - def __init__(self, lhs, client): - self.lhs = lhs - self.client = client - - def _com_dict(self, lhs, rhs): - if len(lhs) != len(rhs): - return False - for key, value in six.iteritems(lhs): - if key not in rhs: - return False - rhs_value = rhs[key] - if not self._com(value, rhs_value): - return False - return True - - def _com_list(self, lhs, rhs): - if len(lhs) != len(rhs): - return False - for lhs_value in lhs: - if lhs_value not in rhs: - return False - return True - - def _com(self, lhs, rhs): - if lhs is None: - return rhs is None - if isinstance(lhs, dict): - if not isinstance(rhs, dict): - return False - return self._com_dict(lhs, rhs) - if isinstance(lhs, list): - if not isinstance(rhs, list): - return False - return self._com_list(lhs, rhs) - if isinstance(lhs, tuple): - if not isinstance(rhs, tuple): - return False - return self._com_list(lhs, rhs) - return lhs == rhs - - def equals(self, rhs): - if self.client: - rhs = self.client.deserialize(rhs, 200) - return self._com(self.lhs, rhs) - - def __repr__(self): - if self.client: - return self.client.serialize(self.lhs) - return str(self.lhs) - - -class CLITestV20Base(base.BaseTestCase): - - format = 'json' - test_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' - id_field = 'id' - - def _find_resourceid(self, client, resource, name_or_id, - cmd_resource=None, parent_id=None): - return name_or_id - - def _get_attr_metadata(self): - return self.metadata - client.Client.EXTED_PLURALS.update(constants.PLURALS) - client.Client.EXTED_PLURALS.update({'tags': 'tag'}) - return {'plurals': client.Client.EXTED_PLURALS, - 'xmlns': constants.XML_NS_V20, - constants.EXT_NS: {'prefix': 'http://xxxx.yy.com'}} + shell = gbpshell + client = gbpclient def setUp(self, plurals=None): - """Prepare the test environment.""" super(CLITestV20Base, self).setUp() - client.Client.EXTED_PLURALS.update(constants.PLURALS) - if plurals is not None: - client.Client.EXTED_PLURALS.update(plurals) - self.metadata = {'plurals': client.Client.EXTED_PLURALS, - 'xmlns': constants.XML_NS_V20, - constants.EXT_NS: {'prefix': - 'http://xxxx.yy.com'}} - self.mox = mox.Mox() - self.endurl = ENDURL - self.fake_stdout = FakeStdout() - self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.fake_stdout)) - self.useFixture(fixtures.MonkeyPatch( - 'neutronclient.neutron.v2_0.find_resourceid_by_name_or_id', - self._find_resourceid)) - self.useFixture(fixtures.MonkeyPatch( - 'neutronclient.neutron.v2_0.find_resourceid_by_id', - self._find_resourceid)) - self.useFixture(fixtures.MonkeyPatch( - 'neutronclient.v2_0.client.Client.get_attr_metadata', - self._get_attr_metadata)) - self.client = client.Client(token=TOKEN, endpoint_url=self.endurl) + self.client = gbpclient.Client(token=TOKEN, endpoint_url=self.endurl) def _test_create_resource(self, resource, cmd, name, myid, args, position_names, position_values, @@ -215,19 +70,9 @@ class CLITestV20Base(base.BaseTestCase): self.mox.StubOutWithMock(cmd, "get_client") self.mox.StubOutWithMock(self.client.httpclient, "request") cmd.get_client().MultipleTimes().AndReturn(self.client) - non_admin_status_resources = ['subnet', 'floatingip', 'security_group', - 'security_group_rule', 'qos_queue', - 'network_gateway', 'gateway_device', - 'credential', 'network_profile', - 'policy_profile', 'ikepolicy', - 'ipsecpolicy', 'metering_label', - 'metering_label_rule', 'net_partition'] if not cmd_resource: cmd_resource = resource - if (resource in non_admin_status_resources): - body = {resource: {}, } - else: - body = {resource: {'admin_state_up': admin_state_up, }, } + body = {resource: {}, } if tenant_id: body[resource].update({'tenant_id': tenant_id}) if tags: @@ -245,8 +90,8 @@ class CLITestV20Base(base.BaseTestCase): self.client.format = self.format resstr = self.client.serialize(ress) # url method body - resource_plural = neutronV2_0._get_resource_plural(cmd_resource, - self.client) + resource_plural = gbpV2_0._get_resource_plural(cmd_resource, + self.client) path = getattr(self.client, resource_plural + "_path") if parent_id: path = path % parent_id @@ -264,7 +109,7 @@ class CLITestV20Base(base.BaseTestCase): args.extend(['--request-format', self.format]) self.mox.ReplayAll() cmd_parser = cmd.get_parser('create_' + resource) - shell.run_command(cmd, cmd_parser, args) + gbpshell.run_command(cmd, cmd_parser, args) self.mox.VerifyAll() self.mox.UnsetStubs() _str = self.fake_stdout.make_string() @@ -272,354 +117,10 @@ class CLITestV20Base(base.BaseTestCase): if name: self.assertIn(name, _str) - def _test_list_columns(self, cmd, resources, - resources_out, args=('-f', 'json'), - cmd_resources=None, parent_id=None): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - self.client.format = self.format - if not cmd_resources: - cmd_resources = resources - - resstr = self.client.serialize(resources_out) - - path = getattr(self.client, cmd_resources + "_path") - if parent_id: - path = path % parent_id - self.client.httpclient.request( - end_url(path, format=self.format), 'GET', - body=None, - headers=mox.ContainsKeyValue( - 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) - args = tuple(args) + ('--request-format', self.format) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("list_" + cmd_resources) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - - def _test_list_resources(self, resources, cmd, detail=False, tags=(), - fields_1=(), fields_2=(), page_size=None, - sort_key=(), sort_dir=(), response_contents=None, - base_args=None, path=None, cmd_resources=None, - parent_id=None): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - if not cmd_resources: - cmd_resources = resources - if response_contents is None: - contents = [{self.id_field: 'myid1', }, - {self.id_field: 'myid2', }, ] - else: - contents = response_contents - reses = {resources: contents} - self.client.format = self.format - resstr = self.client.serialize(reses) - # url method body - query = "" - args = base_args if base_args is not None else [] - if detail: - args.append('-D') - args.extend(['--request-format', self.format]) - if fields_1: - for field in fields_1: - args.append('--fields') - args.append(field) - - if tags: - args.append('--') - args.append("--tag") - for tag in tags: - args.append(tag) - if isinstance(tag, unicode): - tag = urllib.quote(tag.encode('utf-8')) - if query: - query += "&tag=" + tag - else: - query = "tag=" + tag - if (not tags) and fields_2: - args.append('--') - if fields_2: - args.append("--fields") - for field in fields_2: - args.append(field) - if detail: - query = query and query + '&verbose=True' or 'verbose=True' - for field in itertools.chain(fields_1, fields_2): - if query: - query += "&fields=" + field - else: - query = "fields=" + field - if page_size: - args.append("--page-size") - args.append(str(page_size)) - if query: - query += "&limit=%s" % page_size - else: - query = "limit=%s" % page_size - if sort_key: - for key in sort_key: - args.append('--sort-key') - args.append(key) - if query: - query += '&' - query += 'sort_key=%s' % key - if sort_dir: - len_diff = len(sort_key) - len(sort_dir) - if len_diff > 0: - sort_dir = tuple(sort_dir) + ('asc',) * len_diff - elif len_diff < 0: - sort_dir = sort_dir[:len(sort_key)] - for dir in sort_dir: - args.append('--sort-dir') - args.append(dir) - if query: - query += '&' - query += 'sort_dir=%s' % dir - if path is None: - path = getattr(self.client, cmd_resources + "_path") - if parent_id: - path = path % parent_id - self.client.httpclient.request( - MyUrlComparator(end_url(path, query, format=self.format), - self.client), - 'GET', - body=None, - headers=mox.ContainsKeyValue( - 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("list_" + cmd_resources) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - if response_contents is None: - self.assertIn('myid1', _str) - return _str - - def _test_list_resources_with_pagination(self, resources, cmd, - cmd_resources=None, - parent_id=None): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - if not cmd_resources: - cmd_resources = resources - - path = getattr(self.client, cmd_resources + "_path") - if parent_id: - path = path % parent_id - fake_query = "marker=myid2&limit=2" - reses1 = {resources: [{'id': 'myid1', }, - {'id': 'myid2', }], - '%s_links' % resources: [{'href': end_url(path, fake_query), - 'rel': 'next'}]} - reses2 = {resources: [{'id': 'myid3', }, - {'id': 'myid4', }]} - self.client.format = self.format - resstr1 = self.client.serialize(reses1) - resstr2 = self.client.serialize(reses2) - self.client.httpclient.request( - end_url(path, "", format=self.format), 'GET', - body=None, - headers=mox.ContainsKeyValue( - 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr1)) - self.client.httpclient.request( - MyUrlComparator(end_url(path, fake_query, format=self.format), - self.client), 'GET', - body=None, - headers=mox.ContainsKeyValue( - 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr2)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("list_" + cmd_resources) - args = ['--request-format', self.format] - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - - def _test_update_resource(self, resource, cmd, myid, args, extrafields, - cmd_resource=None, parent_id=None): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - if not cmd_resource: - cmd_resource = resource - - body = {resource: extrafields} - path = getattr(self.client, cmd_resource + "_path") - if parent_id: - path = path % (parent_id, myid) - else: - path = path % myid - self.client.format = self.format - # Work around for LP #1217791. XML deserializer called from - # MyComparator does not decodes XML string correctly. - if self.format == 'json': - mox_body = MyComparator(body, self.client) - else: - mox_body = self.client.serialize(body) - self.client.httpclient.request( - MyUrlComparator(end_url(path, format=self.format), - self.client), - 'PUT', - body=mox_body, - headers=mox.ContainsKeyValue( - 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), None)) - args.extend(['--request-format', self.format]) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("update_" + cmd_resource) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertIn(myid, _str) - - def _test_show_resource(self, resource, cmd, myid, args, fields=(), - cmd_resource=None, parent_id=None): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - if not cmd_resource: - cmd_resource = resource - - query = "&".join(["fields=%s" % field for field in fields]) - expected_res = {resource: - {self.id_field: myid, - 'name': 'myname', }, } - self.client.format = self.format - resstr = self.client.serialize(expected_res) - path = getattr(self.client, cmd_resource + "_path") - if parent_id: - path = path % (parent_id, myid) - else: - path = path % myid - self.client.httpclient.request( - end_url(path, query, format=self.format), 'GET', - body=None, - headers=mox.ContainsKeyValue( - 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) - args.extend(['--request-format', self.format]) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("show_" + cmd_resource) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertIn(myid, _str) - self.assertIn('myname', _str) - - def _test_delete_resource(self, resource, cmd, myid, args, - cmd_resource=None, parent_id=None): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - if not cmd_resource: - cmd_resource = resource - path = getattr(self.client, cmd_resource + "_path") - if parent_id: - path = path % (parent_id, myid) - else: - path = path % (myid) - self.client.httpclient.request( - end_url(path, format=self.format), 'DELETE', - body=None, - headers=mox.ContainsKeyValue( - 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), None)) - args.extend(['--request-format', self.format]) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("delete_" + cmd_resource) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertIn(myid, _str) - - def _test_update_resource_action(self, resource, cmd, myid, action, args, - body, retval=None, cmd_resource=None): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - if not cmd_resource: - cmd_resource = resource - path = getattr(self.client, cmd_resource + "_path") - path_action = '%s/%s' % (myid, action) - self.client.httpclient.request( - end_url(path % path_action, format=self.format), 'PUT', - body=MyComparator(body, self.client), - headers=mox.ContainsKeyValue( - 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), retval)) - args.extend(['--request-format', self.format]) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("delete_" + cmd_resource) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertIn(myid, _str) - class ClientV2TestJson(CLITestV20Base): - def test_do_request_unicode(self): - self.client.format = self.format - self.mox.StubOutWithMock(self.client.httpclient, "request") - unicode_text = u'\u7f51\u7edc' - # url with unicode - action = u'/test' - expected_action = action.encode('utf-8') - # query string with unicode - params = {'test': unicode_text} - expect_query = urllib.urlencode({'test': - unicode_text.encode('utf-8')}) - # request body with unicode - body = params - expect_body = self.client.serialize(body) - # headers with unicode - self.client.httpclient.auth_token = unicode_text - expected_auth_token = unicode_text.encode('utf-8') - self.client.httpclient.request( - end_url(expected_action, query=expect_query, format=self.format), - 'PUT', body=expect_body, - headers=mox.ContainsKeyValue( - 'X-Auth-Token', - expected_auth_token)).AndReturn((MyResp(200), expect_body)) - - self.mox.ReplayAll() - res_body = self.client.do_request('PUT', action, body=body, - params=params) - self.mox.VerifyAll() - self.mox.UnsetStubs() - - # test response with unicode - self.assertEqual(res_body, body) - - def test_do_request_error_without_response_body(self): - self.client.format = self.format - self.mox.StubOutWithMock(self.client.httpclient, "request") - params = {'test': 'value'} - expect_query = six.moves.urllib.parse.urlencode(params) - self.client.httpclient.auth_token = 'token' - - self.client.httpclient.request( - MyUrlComparator(end_url( - '/test', query=expect_query, format=self.format), self.client), - 'PUT', body='', - headers=mox.ContainsKeyValue('X-Auth-Token', 'token') - ).AndReturn((MyResp(400, reason='An error'), '')) - - self.mox.ReplayAll() - error = self.assertRaises(exceptions.NeutronClientException, - self.client.do_request, 'PUT', '/test', - body='', params=params) - self.assertEqual("An error", str(error)) - self.mox.VerifyAll() - self.mox.UnsetStubs() - - -class ClientV2UnicodeTestXML(ClientV2TestJson): - format = 'xml' + pass class CLITestV20ExceptionHandler(CLITestV20Base): @@ -634,7 +135,7 @@ class CLITestV20ExceptionHandler(CLITestV20Base): 'detail': error_detail}} e = self.assertRaises(expected_exception, - client.exception_handler_v20, + gbpclient.exception_handler_v20, status_code, error_content) self.assertEqual(status_code, e.status_code) @@ -645,44 +146,13 @@ class CLITestV20ExceptionHandler(CLITestV20Base): expected_msg = error_msg self.assertEqual(expected_msg, e.message) - def test_exception_handler_v20_ip_address_in_use(self): - err_msg = ('Unable to complete operation for network ' - 'fake-network-uuid. The IP address fake-ip is in use.') - self._test_exception_handler_v20( - exceptions.IpAddressInUseClient, 409, err_msg, - 'IpAddressInUse', err_msg, '') - def test_exception_handler_v20_neutron_known_error(self): - known_error_map = [ - ('NetworkNotFound', exceptions.NetworkNotFoundClient, 404), - ('PortNotFound', exceptions.PortNotFoundClient, 404), - ('NetworkInUse', exceptions.NetworkInUseClient, 409), - ('PortInUse', exceptions.PortInUseClient, 409), - ('StateInvalid', exceptions.StateInvalidClient, 400), - ('IpAddressInUse', exceptions.IpAddressInUseClient, 409), - ('IpAddressGenerationFailure', - exceptions.IpAddressGenerationFailureClient, 409), - ('MacAddressInUse', exceptions.MacAddressInUseClient, 409), - ('ExternalIpAddressExhausted', - exceptions.ExternalIpAddressExhaustedClient, 400), - ('OverQuota', exceptions.OverQuotaClient, 409), - ] - - error_msg = 'dummy exception message' - error_detail = 'sample detail' - for server_exc, client_exc, status_code in known_error_map: - self._test_exception_handler_v20( - client_exc, status_code, - error_msg + '\n' + error_detail, - server_exc, error_msg, error_detail) + # TODO(Sumit): This needs to be adapted for GBP + pass def test_exception_handler_v20_neutron_known_error_without_detail(self): - error_msg = 'Network not found' - error_detail = '' - self._test_exception_handler_v20( - exceptions.NetworkNotFoundClient, 404, - error_msg, - 'NetworkNotFound', error_msg, error_detail) + # TODO(Sumit): This needs to be adapted for GBP + pass def test_exception_handler_v20_unknown_error_to_per_code_exception(self): for status_code, client_exc in exceptions.HTTP_EXCEPTION_MAP.items(): diff --git a/gbpclient/tests/unit/test_cli20_networkservicepolicy.py b/gbpclient/tests/unit/test_cli20_networkservicepolicy.py new file mode 100644 index 0000000..18f82d5 --- /dev/null +++ b/gbpclient/tests/unit/test_cli20_networkservicepolicy.py @@ -0,0 +1,137 @@ +# 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 groupbasedpolicy as gbp +from gbpclient.tests.unit import test_cli20 + + +class CLITestV20NetworkServicePolicyJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20NetworkServicePolicyJSON, self).setUp() + + def test_create_nsp_with_mandatory_params(self): + """network-service-policy-create with mandatory params.""" + resource = 'network_service_policy' + cmd = gbp.CreateNetworkServicePolicy(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_network_service_policy_with_all_params(self): + """network-service-policy-create with all params.""" + resource = 'network_service_policy' + cmd = gbp.CreateNetworkServicePolicy(test_cli20.MyApp(sys.stdout), + None) + name = 'myname' + tenant_id = 'mytenant' + description = 'Mynsp' + my_id = 'someid' + network_svc_params = "type=ip_single,name=vip,value=self_subnet" + args = ['--tenant_id', tenant_id, + '--description', description, + '--network-service-params', network_svc_params, + name] + position_names = ['name', 'description', 'network_service_params'] + net_params = [{"type": "ip_single", "name": "vip", + "value": "self_subnet"}] + position_values = [name, description, net_params] + self._test_create_resource(resource, cmd, name, my_id, args, + position_names, position_values, + tenant_id=tenant_id) + + def test_list_network_service_policies(self): + """network-sercvice-policy-list.""" + resources = 'network_service_policies' + cmd = gbp.ListNetworkServicePolicy(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources(resources, cmd, True) + + def test_list_network_service_policies_with_pagination(self): + """network-sercvice-policy-list.""" + resources = 'network_service_policies' + cmd = gbp.ListNetworkServicePolicy(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_network_sercice_policies_sort(self): + """network-service-policy-list --sort-key name --sort-key id + --sort-key asc --sort-key desc + """ + resources = 'network_service_policies' + cmd = gbp.ListNetworkServicePolicy(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_network_service_polices_limit(self): + """network-service-policy-list -P.""" + resources = 'network_service_policies' + cmd = gbp.ListNetworkServicePolicy(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_network_service_policy_id(self): + """network-service-policy-show test_id.""" + resource = 'network_service_policy' + cmd = gbp.ShowNetworkServicePolicy(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_network_service_policy_id_name(self): + """network-service-policy-show.""" + resource = 'network_service_policy' + cmd = gbp.ShowNetworkServicePolicy(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_network_service_policy(self): + """network-service-policy-update myid --name myname --tags a b.""" + resource = 'network_service_policy' + cmd = gbp.UpdateNetworkServicePolicy(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_network_service_policy_with_allparams(self): + resource = 'network_service_policy' + new_name = "new_name" + cmd = gbp.UpdateNetworkServicePolicy(test_cli20.MyApp(sys.stdout), + None) + body = { + 'name': new_name + } + args = ['myid', '--name', new_name, '--request-format', 'json'] + self._test_update_resource(resource, cmd, 'myid', args, body) + + def test_delete_network_service_policy(self): + """network-service-policy-delete my-id.""" + resource = 'network_service_policy' + cmd = gbp.DeleteNetworkServicePolicy(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_policyaction.py b/gbpclient/tests/unit/test_cli20_policyaction.py new file mode 100644 index 0000000..dc922e7 --- /dev/null +++ b/gbpclient/tests/unit/test_cli20_policyaction.py @@ -0,0 +1,135 @@ +# 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 groupbasedpolicy as gbp +from gbpclient.tests.unit import test_cli20 + + +class CLITestV20PolicyActionJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20PolicyActionJSON, self).setUp() + + def test_create_policy_action_with_mandatory_params(self): + """grouppolicy-policy-action-create with all mandatory params.""" + resource = 'policy_action' + cmd = gbp.CreatePolicyAction(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_policy_action_with_all_params(self): + """grouppolicy-policy-action-create with all params.""" + resource = 'policy_action' + cmd = gbp.CreatePolicyAction(test_cli20.MyApp(sys.stdout), None) + name = 'my-name' + tenant_id = 'my-tenant' + description = 'My PolicyAction' + my_id = 'my-id' + action_type = "allow" + action_value = "1234" + args = ['--tenant-id', tenant_id, + '--description', description, + '--action-type', action_type, + '--action-value', action_value, + 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, + description=description, + action_type=action_type, + action_value=action_value) + + def test_list_policy_actions(self): + """grouppolicy-policy-action-list.""" + resources = 'policy_actions' + cmd = gbp.ListPolicyAction(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_policy_actions_pagination(self): + """grouppolicy-policy-action-list.""" + resources = 'policy_actions' + cmd = gbp.ListPolicyAction(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_policy_actions_sort(self): + """grouppolicy-policy-action-list --sort-key name --sort-key id + --sort-key asc --sort-key desc + """ + resources = 'policy_actions' + cmd = gbp.ListPolicyAction(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_policy_actions_limit(self): + """grouppolicy-policy-action-list -P.""" + resources = 'policy_actions' + cmd = gbp.ListPolicyAction(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_policy_action_id(self): + """grouppolicy-policy-action-show test_id.""" + resource = 'policy_action' + cmd = gbp.ShowPolicyAction(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_policy_action_id_name(self): + """grouppolicy-policy-action-show.""" + resource = 'policy_action' + cmd = gbp.ShowPolicyAction(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_policy_action(self): + """grouppolicy-policy-action-update myid --name myname --tags a b.""" + resource = 'policy_action' + cmd = gbp.UpdatePolicyAction(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_policy_action_with_allparams(self): + resource = 'policy_action' + action_type = "allow" + action_value = "1234" + cmd = gbp.UpdatePolicyAction(test_cli20.MyApp(sys.stdout), None) + body = { + 'action_type': action_type, + 'action_value': action_value + } + args = ['myid', + '--action-type', action_type, + '--action-value', action_value, ] + self._test_update_resource(resource, cmd, 'myid', args, body) + + def test_delete_policy_action(self): + """grouppolicy-policy-action-delete my-id.""" + resource = 'policy_action' + cmd = gbp.DeletePolicyAction(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_policyclassifier.py b/gbpclient/tests/unit/test_cli20_policyclassifier.py new file mode 100644 index 0000000..32cc4a7 --- /dev/null +++ b/gbpclient/tests/unit/test_cli20_policyclassifier.py @@ -0,0 +1,141 @@ +# 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 groupbasedpolicy as gbp +from gbpclient.tests.unit import test_cli20 + + +class CLITestV20PolicyClassifierJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20PolicyClassifierJSON, self).setUp() + + def test_create_policy_classifier_with_mandatory_params(self): + """grouppolicy-policy-classifier-create with all mandatory params.""" + resource = 'policy_classifier' + cmd = gbp.CreatePolicyClassifier(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_policy_classifier_with_all_params(self): + """grouppolicy-policy-classifier-create with all params.""" + resource = 'policy_classifier' + cmd = gbp.CreatePolicyClassifier(test_cli20.MyApp(sys.stdout), None) + name = 'my-name' + tenant_id = 'my-tenant' + description = 'My PolicyClassifier' + my_id = 'my-id' + protocol = 'tcp' + port_range = '10-80' + direction = 'in' + args = ['--tenant-id', tenant_id, + '--description', description, + '--protocol', protocol, + '--port-range', port_range, + '--direction', direction, + 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, + description=description, + protocol=protocol, + port_range=port_range, + direction=direction) + + def test_list_policy_classifiers(self): + """grouppolicy-policy-classifier-list.""" + resources = 'policy_classifiers' + cmd = gbp.ListPolicyClassifier(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_policy_classifiers_pagination(self): + """grouppolicy-policy-classifier-list.""" + resources = 'policy_classifiers' + cmd = gbp.ListPolicyClassifier(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_policy_classifiers_sort(self): + """grouppolicy-policy-classifier-list --sort-key name --sort-key id + --sort-key asc --sort-key desc + """ + resources = 'policy_classifiers' + cmd = gbp.ListPolicyClassifier(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_policy_classifiers_limit(self): + """grouppolicy-policy-classifier-list -P.""" + resources = 'policy_classifiers' + cmd = gbp.ListPolicyClassifier(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_policy_classifier_id(self): + """grouppolicy-policy-classifier-show test_id.""" + resource = 'policy_classifier' + cmd = gbp.ShowPolicyClassifier(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_policy_classifier_id_name(self): + """grouppolicy-policy-classifier-show.""" + resource = 'policy_classifier' + cmd = gbp.ShowPolicyClassifier(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_policy_classifier(self): + """grouppolicy-policy-classifier-update myid --name myname --tags a b. + """ + resource = 'policy_classifier' + cmd = gbp.UpdatePolicyClassifier(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_policy_classifier_with_allparams(self): + resource = 'policy_classifier' + protocol = 'tcp' + port_range = '10-80' + direction = 'in' + cmd = gbp.UpdatePolicyClassifier(test_cli20.MyApp(sys.stdout), None) + body = { + 'protocol': protocol, + 'port_range': port_range, + 'direction': direction + } + args = ['myid', '--protocol', protocol, + '--port-range', port_range, + '--direction', direction, ] + self._test_update_resource(resource, cmd, 'myid', args, body) + + def test_delete_policy_classifier(self): + """grouppolicy-policy-classifier-delete my-id.""" + resource = 'policy_classifier' + cmd = gbp.DeletePolicyClassifier(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_policyrule.py b/gbpclient/tests/unit/test_cli20_policyrule.py new file mode 100644 index 0000000..c07beb9 --- /dev/null +++ b/gbpclient/tests/unit/test_cli20_policyrule.py @@ -0,0 +1,142 @@ +# 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 groupbasedpolicy as gbp +from gbpclient.tests.unit import test_cli20 + + +class CLITestV20PolicyRuleJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20PolicyRuleJSON, self).setUp() + + def test_create_policy_rule_with_mandatory_params(self): + """grouppolicy-policy-rule-create with all mandatory params.""" + resource = 'policy_rule' + cmd = gbp.CreatePolicyRule(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_policy_rule_with_all_params(self): + """grouppolicy-policy-rule-create with all params.""" + resource = 'policy_rule' + cmd = gbp.CreatePolicyRule(test_cli20.MyApp(sys.stdout), None) + name = 'my-name' + tenant_id = 'my-tenant' + description = 'My PolicyRule' + my_id = 'my-id' + enabled = True + policy_classifier_id = 'pc-id' + policy_actions_res = ["pa1", "pa2"] + policy_actions_arg = "pa1 pa2" + args = ['--tenant-id', tenant_id, + '--description', description, + '--enabled', "True", + '--classifier', policy_classifier_id, + '--actions', policy_actions_arg, + 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, + description=description, + enabled=enabled, + policy_classifier_id=policy_classifier_id, + policy_actions=policy_actions_res) + + def test_list_policy_rules(self): + """grouppolicy-policy-rule-list.""" + resources = 'policy_rules' + cmd = gbp.ListPolicyRule(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_policy_rules_pagination(self): + """grouppolicy-policy-rule-list.""" + resources = 'policy_rules' + cmd = gbp.ListPolicyRule(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_policy_rules_sort(self): + """grouppolicy-policy-rule-list --sort-key name --sort-key id + --sort-key asc --sort-key desc + """ + resources = 'policy_rules' + cmd = gbp.ListPolicyRule(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_policy_rules_limit(self): + """grouppolicy-policy-rule-list -P.""" + resources = 'policy_rules' + cmd = gbp.ListPolicyRule(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_policy_classifier_id(self): + """grouppolicy-policy-rule-show test_id.""" + resource = 'policy_rule' + cmd = gbp.ShowPolicyRule(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_policy_classifier_id_name(self): + """grouppolicy-policy-rule-show.""" + resource = 'policy_rule' + cmd = gbp.ShowPolicyRule(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_policy_rule(self): + """grouppolicy-policy-rule-update myid --name myname --tags a b.""" + resource = 'policy_rule' + cmd = gbp.UpdatePolicyRule(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_policy_rule_with_allparams(self): + resource = 'policy_rule' + enabled = True + policy_classifier_id = 'pc-id' + policy_actions_res = ["pa1", "pa2"] + policy_actions_arg = "pa1 pa2" + cmd = gbp.UpdatePolicyRule(test_cli20.MyApp(sys.stdout), None) + body = { + 'policy_classifier_id': policy_classifier_id, + 'enabled': enabled, + 'policy_actions': policy_actions_res + } + args = ['myid', '--enabled', "True", + '--classifier', policy_classifier_id, + '--actions', policy_actions_arg, ] + self._test_update_resource(resource, cmd, 'myid', args, body) + + def test_delete_policy_classifier(self): + """grouppolicy-policy-rule-delete my-id.""" + resource = 'policy_rule' + cmd = gbp.DeletePolicyRule(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/v2_0/__init__.py b/gbpclient/v2_0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gbpclient/v2_0/client.py b/gbpclient/v2_0/client.py new file mode 100644 index 0000000..9836bf7 --- /dev/null +++ b/gbpclient/v2_0/client.py @@ -0,0 +1,624 @@ +# 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 logging +import time +import urllib + +from neutronclient import client +from neutronclient.common import _ +from neutronclient.common import constants +from neutronclient.common import exceptions +from neutronclient.common import serializer +from neutronclient.common import utils +import requests +import six.moves.urllib.parse as urlparse + + +_logger = logging.getLogger(__name__) + + +def exception_handler_v20(status_code, error_content): + """Exception handler for API v2.0 client + + This routine generates the appropriate + Neutron exception according to the contents of the + response body + + :param status_code: HTTP error status code + :param error_content: deserialized body of error response + """ + error_dict = None + if isinstance(error_content, dict): + error_dict = error_content.get('NeutronError') + # Find real error type + bad_neutron_error_flag = False + if error_dict: + # If Neutron key is found, it will definitely contain + # a 'message' and 'type' keys? + try: + error_type = error_dict['type'] + error_message = error_dict['message'] + if error_dict['detail']: + error_message += "\n" + error_dict['detail'] + except Exception: + bad_neutron_error_flag = True + if not bad_neutron_error_flag: + # If corresponding exception is defined, use it. + client_exc = getattr(exceptions, '%sClient' % error_type, None) + # Otherwise look up per status-code client exception + if not client_exc: + client_exc = exceptions.HTTP_EXCEPTION_MAP.get(status_code) + if client_exc: + raise client_exc(message=error_message, + status_code=status_code) + else: + raise exceptions.NeutronClientException( + status_code=status_code, message=error_message) + else: + raise exceptions.NeutronClientException(status_code=status_code, + message=error_dict) + else: + message = None + if isinstance(error_content, dict): + message = error_content.get('message') + if message: + raise exceptions.NeutronClientException(status_code=status_code, + message=message) + + # If we end up here the exception was not a neutron error + msg = "%s-%s" % (status_code, error_content) + raise exceptions.NeutronClientException(status_code=status_code, + message=msg) + + +class APIParamsCall(object): + """A Decorator to add support for format and tenant overriding + and filters + """ + def __init__(self, function): + self.function = function + + def __get__(self, instance, owner): + def with_params(*args, **kwargs): + _format = instance.format + if 'format' in kwargs: + instance.format = kwargs['format'] + ret = self.function(instance, *args, **kwargs) + instance.format = _format + return ret + return with_params + + +class Client(object): + """Client for the GBP API. + + :param string username: Username for authentication. (optional) + :param string user_id: User ID for authentication. (optional) + :param string password: Password for authentication. (optional) + :param string token: Token for authentication. (optional) + :param string tenant_name: Tenant name. (optional) + :param string tenant_id: Tenant id. (optional) + :param string auth_url: Keystone service endpoint for authorization. + :param string service_type: Network service type to pull from the + keystone catalog (e.g. 'network') (optional) + :param string endpoint_type: Network service endpoint type to pull from the + keystone catalog (e.g. 'publicURL', + 'internalURL', or 'adminURL') (optional) + :param string region_name: Name of a region to select when choosing an + endpoint from the service catalog. + :param string endpoint_url: A user-supplied endpoint URL for the neutron + service. Lazy-authentication is possible for API + service calls if endpoint is set at + instantiation.(optional) + :param integer timeout: Allows customization of the timeout for client + http requests. (optional) + :param bool insecure: SSL certificate validation. (optional) + :param string ca_cert: SSL CA bundle file to use. (optional) + :param integer retries: How many times idempotent (GET, PUT, DELETE) + requests to Neutron server should be retried if + they fail (default: 0). + :param bool raise_errors: If True then exceptions caused by connection + failure are propagated to the caller. + (default: True) + :param session: Keystone client auth session to use. (optional) + :param auth: Keystone auth plugin to use. (optional) + + Example:: + + from gbpclient.v2_0 import client + gbp = client.Client(username=USER, + password=PASS, + tenant_name=TENANT_NAME, + auth_url=KEYSTONE_URL) + + ptgs = gbp.list_policy_target_groups() + ... + + """ + + endpoints_path = "/grouppolicy/endpoints" + endpoint_path = "/grouppolicy/endpoints/%s" + endpoint_groups_path = "/grouppolicy/endpoint_groups" + endpoint_group_path = "/grouppolicy/endpoint_groups/%s" + l2_policies_path = "/grouppolicy/l2_policies" + l2_policy_path = "/grouppolicy/l2_policies/%s" + l3_policies_path = "/grouppolicy/l3_policies" + l3_policy_path = "/grouppolicy/l3_policies/%s" + network_service_policies_path = "/grouppolicy/network_service_policies" + network_service_policy_path = "/grouppolicy/network_service_policies/%s" + policy_classifiers_path = "/grouppolicy/policy_classifiers" + policy_classifier_path = "/grouppolicy/policy_classifiers/%s" + policy_actions_path = "/grouppolicy/policy_actions" + policy_action_path = "/grouppolicy/policy_actions/%s" + policy_rules_path = "/grouppolicy/policy_rules" + policy_rule_path = "/grouppolicy/policy_rules/%s" + contracts_path = "/grouppolicy/contracts" + contract_path = "/grouppolicy/contracts/%s" + + # API has no way to report plurals, so we have to hard code them + EXTED_PLURALS = {'endpoints': 'endpoint', + 'endpoint_groups': 'endpoint_group', + 'l2_policies': 'l2_policy', + 'l3_policies': 'l3_policy', + 'network_service_policies': 'network_service_policy', + 'policy_classifiers': 'policy_classifier', + 'policy_actions': 'policy_action', + 'policy_rules': 'policy_rule', + 'contracts': 'contract', + } + # 8192 Is the default max URI len for eventlet.wsgi.server + MAX_URI_LEN = 8192 + + def get_attr_metadata(self): + if self.format == 'json': + return {} + old_request_format = self.format + self.format = 'json' + exts = self.list_extensions()['extensions'] + self.format = old_request_format + ns = dict([(ext['alias'], ext['namespace']) for ext in exts]) + self.EXTED_PLURALS.update(constants.PLURALS) + return {'plurals': self.EXTED_PLURALS, + 'xmlns': constants.XML_NS_V20, + constants.EXT_NS: ns} + + @APIParamsCall + def list_extensions(self, **_params): + """Fetch a list of all exts on server side.""" + return self.get(self.extensions_path, params=_params) + + @APIParamsCall + def show_extension(self, ext_alias, **_params): + """Fetch a list of all exts on server side.""" + return self.get(self.extension_path % ext_alias, params=_params) + + @APIParamsCall + def list_endpoints(self, retrieve_all=True, **_params): + """Fetches a list of all endpoints for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('endpoints', self.endpoints_path, retrieve_all, + **_params) + + @APIParamsCall + def show_endpoint(self, endpoint, **_params): + """Fetches information of a certain endpoint.""" + return self.get(self.endpoint_path % (endpoint), params=_params) + + @APIParamsCall + def create_endpoint(self, body=None): + """Creates a new endpoint.""" + return self.post(self.endpoints_path, body=body) + + @APIParamsCall + def update_endpoint(self, endpoint, body=None): + """Updates a endpoint.""" + return self.put(self.endpoint_path % (endpoint), body=body) + + @APIParamsCall + def delete_endpoint(self, endpoint): + """Deletes the specified endpoint.""" + return self.delete(self.endpoint_path % (endpoint)) + + @APIParamsCall + def list_endpoint_groups(self, retrieve_all=True, **_params): + """Fetches a list of all endpoint_groups for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('endpoint_groups', self.endpoint_groups_path, + retrieve_all, **_params) + + @APIParamsCall + def show_endpoint_group(self, endpoint_group, **_params): + """Fetches information of a certain endpoint_group.""" + return self.get(self.endpoint_group_path % (endpoint_group), + params=_params) + + @APIParamsCall + def create_endpoint_group(self, body=None): + """Creates a new endpoint_group.""" + return self.post(self.endpoint_groups_path, body=body) + + @APIParamsCall + def update_endpoint_group(self, endpoint_group, body=None): + """Updates a endpoint_group.""" + return self.put(self.endpoint_group_path % (endpoint_group), + body=body) + + @APIParamsCall + def delete_endpoint_group(self, endpoint_group): + """Deletes the specified endpoint_group.""" + return self.delete(self.endpoint_group_path % (endpoint_group)) + + @APIParamsCall + def list_l2_policies(self, retrieve_all=True, **_params): + """Fetches a list of all l2_policies for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('l2_policies', self.l2_policies_path, + retrieve_all, **_params) + + @APIParamsCall + def show_l2_policy(self, l2_policy, **_params): + """Fetches information of a certain l2_policy.""" + return self.get(self.l2_policy_path % (l2_policy), + params=_params) + + @APIParamsCall + def create_l2_policy(self, body=None): + """Creates a new l2_policy.""" + return self.post(self.l2_policies_path, body=body) + + @APIParamsCall + def update_l2_policy(self, l2_policy, body=None): + """Updates a l2_policy.""" + return self.put(self.l2_policy_path % (l2_policy), body=body) + + @APIParamsCall + def delete_l2_policy(self, l2_policy): + """Deletes the specified l2_policy.""" + return self.delete(self.l2_policy_path % (l2_policy)) + + @APIParamsCall + def list_network_service_policies(self, retrieve_all=True, **_params): + """Fetches a list of all network_service_policies for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('network_service_policies', + self.network_service_policies_path, + retrieve_all, **_params) + + @APIParamsCall + def show_network_service_policy(self, network_service_policy, **_params): + """Fetches information of a certain network_service_policy.""" + return self.get( + self.network_service_policy_path % (network_service_policy), + params=_params) + + @APIParamsCall + def create_network_service_policy(self, body=None): + """Creates a new network_service_policy.""" + return self.post(self.network_service_policies_path, body=body) + + @APIParamsCall + def update_network_service_policy(self, network_service_policy, body=None): + """Updates a network_service_policy.""" + return self.put( + self.network_service_policy_path % (network_service_policy), + body=body) + + @APIParamsCall + def delete_network_service_policy(self, network_service_policy): + """Deletes the specified network_service_policy.""" + return self.delete( + self.network_service_policy_path % (network_service_policy)) + + @APIParamsCall + def list_l3_policies(self, retrieve_all=True, **_params): + """Fetches a list of all l3_policies for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('l3_policies', self.l3_policies_path, + retrieve_all, **_params) + + @APIParamsCall + def show_l3_policy(self, l3_policy, **_params): + """Fetches information of a certain l3_policy.""" + return self.get(self.l3_policy_path % (l3_policy), + params=_params) + + @APIParamsCall + def create_l3_policy(self, body=None): + """Creates a new l3_policy.""" + return self.post(self.l3_policies_path, body=body) + + @APIParamsCall + def update_l3_policy(self, l3_policy, body=None): + """Updates a l3_policy.""" + return self.put(self.l3_policy_path % (l3_policy), + body=body) + + @APIParamsCall + def delete_l3_policy(self, l3_policy): + """Deletes the specified l3_policy.""" + return self.delete(self.l3_policy_path % (l3_policy)) + + @APIParamsCall + def list_policy_classifiers(self, retrieve_all=True, **_params): + """Fetches a list of all policy_classifiers for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('policy_classifiers', self.policy_classifiers_path, + retrieve_all, **_params) + + @APIParamsCall + def show_policy_classifier(self, policy_classifier, **_params): + """Fetches information of a certain policy_classifier.""" + return self.get(self.policy_classifier_path % (policy_classifier), + params=_params) + + @APIParamsCall + def create_policy_classifier(self, body=None): + """Creates a new policy_classifier.""" + return self.post(self.policy_classifiers_path, body=body) + + @APIParamsCall + def update_policy_classifier(self, policy_classifier, body=None): + """Updates a policy_classifier.""" + return self.put(self.policy_classifier_path % (policy_classifier), + body=body) + + @APIParamsCall + def delete_policy_classifier(self, policy_classifier): + """Deletes the specified policy_classifier.""" + return self.delete(self.policy_classifier_path % (policy_classifier)) + + @APIParamsCall + def list_policy_actions(self, retrieve_all=True, **_params): + """Fetches a list of all policy_actions for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('policy_actions', self.policy_actions_path, + retrieve_all, **_params) + + @APIParamsCall + def show_policy_action(self, policy_action, **_params): + """Fetches information of a certain policy_action.""" + return self.get(self.policy_action_path % (policy_action), + params=_params) + + @APIParamsCall + def create_policy_action(self, body=None): + """Creates a new policy_action.""" + return self.post(self.policy_actions_path, body=body) + + @APIParamsCall + def update_policy_action(self, policy_action, body=None): + """Updates a policy_action.""" + return self.put(self.policy_action_path % (policy_action), body=body) + + @APIParamsCall + def delete_policy_action(self, policy_action): + """Deletes the specified policy_action.""" + return self.delete(self.policy_action_path % (policy_action)) + + @APIParamsCall + def list_policy_rules(self, retrieve_all=True, **_params): + """Fetches a list of all policy_rules for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('policy_rules', self.policy_rules_path, retrieve_all, + **_params) + + @APIParamsCall + def show_policy_rule(self, policy_rule, **_params): + """Fetches information of a certain policy_rule.""" + return self.get(self.policy_rule_path % (policy_rule), params=_params) + + @APIParamsCall + def create_policy_rule(self, body=None): + """Creates a new policy_rule.""" + return self.post(self.policy_rules_path, body=body) + + @APIParamsCall + def update_policy_rule(self, policy_rule, body=None): + """Updates a policy_rule.""" + return self.put(self.policy_rule_path % (policy_rule), body=body) + + @APIParamsCall + def delete_policy_rule(self, policy_rule): + """Deletes the specified policy_rule.""" + return self.delete(self.policy_rule_path % (policy_rule)) + + @APIParamsCall + def list_contracts(self, retrieve_all=True, **_params): + """Fetches a list of all contracts for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('contracts', self.contracts_path, retrieve_all, + **_params) + + @APIParamsCall + def show_contract(self, contract, **_params): + """Fetches information of a certain contract.""" + return self.get(self.contract_path % (contract), params=_params) + + @APIParamsCall + def create_contract(self, body=None): + """Creates a new contract.""" + return self.post(self.contracts_path, body=body) + + @APIParamsCall + def update_contract(self, contract, body=None): + """Updates a contract.""" + return self.put(self.contract_path % (contract), body=body) + + @APIParamsCall + def delete_contract(self, contract): + """Deletes the specified contract.""" + return self.delete(self.contract_path % (contract)) + + def __init__(self, **kwargs): + """Initialize a new client for the GBP v2.0 API.""" + super(Client, self).__init__() + self.retries = kwargs.pop('retries', 0) + self.raise_errors = kwargs.pop('raise_errors', True) + self.httpclient = client.construct_http_client(**kwargs) + self.version = '2.0' + self.format = 'json' + self.action_prefix = "/v%s" % (self.version) + self.retry_interval = 1 + + def _handle_fault_response(self, status_code, response_body): + # Create exception with HTTP status code and message + _logger.debug("Error message: %s", response_body) + # Add deserialized error message to exception arguments + try: + des_error_body = self.deserialize(response_body, status_code) + except Exception: + # If unable to deserialized body it is probably not a + # Neutron error + des_error_body = {'message': response_body} + # Raise the appropriate exception + exception_handler_v20(status_code, des_error_body) + + def _check_uri_length(self, action): + uri_len = len(self.httpclient.endpoint_url) + len(action) + if uri_len > self.MAX_URI_LEN: + raise exceptions.RequestURITooLong( + excess=uri_len - self.MAX_URI_LEN) + + def do_request(self, method, action, body=None, headers=None, params=None): + # Add format and tenant_id + action += ".%s" % self.format + action = self.action_prefix + action + if type(params) is dict and params: + params = utils.safe_encode_dict(params) + action += '?' + urllib.urlencode(params, doseq=1) + # Ensure client always has correct uri - do not guesstimate anything + self.httpclient.authenticate_and_fetch_endpoint_url() + self._check_uri_length(action) + + if body: + body = self.serialize(body) + self.httpclient.content_type = self.content_type() + resp, replybody = self.httpclient.do_request(action, method, body=body) + status_code = resp.status_code + if status_code in (requests.codes.ok, + requests.codes.created, + requests.codes.accepted, + requests.codes.no_content): + return self.deserialize(replybody, status_code) + else: + if not replybody: + replybody = resp.reason + self._handle_fault_response(status_code, replybody) + + def get_auth_info(self): + return self.httpclient.get_auth_info() + + def serialize(self, data): + """Serializes a dictionary into either XML or JSON. + + A dictionary with a single key can be passed and + it can contain any structure. + """ + if data is None: + return None + elif type(data) is dict: + return serializer.Serializer( + self.get_attr_metadata()).serialize(data, self.content_type()) + else: + raise Exception(_("Unable to serialize object of type = '%s'") % + type(data)) + + def deserialize(self, data, status_code): + """Deserializes an XML or JSON string into a dictionary.""" + if status_code == 204: + return data + return serializer.Serializer(self.get_attr_metadata()).deserialize( + data, self.content_type())['body'] + + def content_type(self, _format=None): + """Returns the mime-type for either 'xml' or 'json'. + + Defaults to the currently set format. + """ + _format = _format or self.format + return "application/%s" % (_format) + + def retry_request(self, method, action, body=None, + headers=None, params=None): + """Call do_request with the default retry configuration. + + Only idempotent requests should retry failed connection attempts. + :raises: ConnectionFailed if the maximum # of retries is exceeded + """ + max_attempts = self.retries + 1 + for i in range(max_attempts): + try: + return self.do_request(method, action, body=body, + headers=headers, params=params) + except exceptions.ConnectionFailed: + # Exception has already been logged by do_request() + if i < self.retries: + _logger.debug('Retrying connection to Neutron service') + time.sleep(self.retry_interval) + elif self.raise_errors: + raise + + if self.retries: + msg = (_("Failed to connect to Neutron server after %d attempts") + % max_attempts) + else: + msg = _("Failed to connect Neutron server") + + raise exceptions.ConnectionFailed(reason=msg) + + def delete(self, action, body=None, headers=None, params=None): + return self.retry_request("DELETE", action, body=body, + headers=headers, params=params) + + def get(self, action, body=None, headers=None, params=None): + return self.retry_request("GET", action, body=body, + headers=headers, params=params) + + def post(self, action, body=None, headers=None, params=None): + # Do not retry POST requests to avoid the orphan objects problem. + return self.do_request("POST", action, body=body, + headers=headers, params=params) + + def put(self, action, body=None, headers=None, params=None): + return self.retry_request("PUT", action, body=body, + headers=headers, params=params) + + def list(self, collection, path, retrieve_all=True, **params): + if retrieve_all: + res = [] + for r in self._pagination(collection, path, **params): + res.extend(r[collection]) + return {collection: res} + else: + return self._pagination(collection, path, **params) + + def _pagination(self, collection, path, **params): + if params.get('page_reverse', False): + linkrel = 'previous' + else: + linkrel = 'next' + next = True + while next: + res = self.get(path, params=params) + yield res + next = False + try: + for link in res['%s_links' % collection]: + if link['rel'] == linkrel: + query_str = urlparse.urlparse(link['href']).query + params = urlparse.parse_qs(query_str) + next = True + break + except KeyError: + break diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..8d465d9 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,227 @@ +#!/bin/bash + +set -eu + +function usage { + echo "Usage: $0 [OPTION]..." + echo "Run Group Based Policy's test suite(s)" + echo "" + echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" + echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment" + echo " -r, --recreate-db Recreate the test database (deprecated, as this is now the default)." + echo " -n, --no-recreate-db Don't recreate the test database." + echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." + echo " -u, --update Update the virtual environment with any newer package versions" + echo " -p, --pep8 Just run PEP8 and HACKING compliance check" + echo " -P, --no-pep8 Don't run static code checks" + echo " -c, --coverage Generate coverage report" + echo " -d, --debug Run tests with testtools instead of testr. This allows you to use the debugger." + echo " -h, --help Print this usage message" + echo " --virtual-env-path Location of the virtualenv directory" + echo " Default: \$(pwd)" + echo " --virtual-env-name Name of the virtualenv directory" + echo " Default: .venv" + echo " --tools-path Location of the tools directory" + echo " Default: \$(pwd)" + echo "" + echo "Note: with no options specified, the script will try to run the tests in a virtual environment," + echo " If no virtualenv is found, the script will ask if you would like to create one. If you " + echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." + exit +} + +function process_options { + i=1 + while [ $i -le $# ]; do + case "${!i}" in + -h|--help) usage;; + -V|--virtual-env) always_venv=1; never_venv=0;; + -N|--no-virtual-env) always_venv=0; never_venv=1;; + -s|--no-site-packages) no_site_packages=1;; + -r|--recreate-db) recreate_db=1;; + -n|--no-recreate-db) recreate_db=0;; + -f|--force) force=1;; + -u|--update) update=1;; + -p|--pep8) just_pep8=1;; + -P|--no-pep8) no_pep8=1;; + -c|--coverage) coverage=1;; + -d|--debug) debug=1;; + --virtual-env-path) + (( i++ )) + venv_path=${!i} + ;; + --virtual-env-name) + (( i++ )) + venv_dir=${!i} + ;; + --tools-path) + (( i++ )) + tools_path=${!i} + ;; + -*) testropts="$testropts ${!i}";; + *) testrargs="$testrargs ${!i}" + esac + (( i++ )) + done +} + +tool_path=${tools_path:-$(pwd)} +venv_path=${venv_path:-$(pwd)} +venv_dir=${venv_name:-.venv} +with_venv=tools/with_venv.sh +always_venv=0 +never_venv=0 +force=0 +no_site_packages=0 +installvenvopts= +testrargs= +testropts= +wrapper="" +just_pep8=0 +no_pep8=0 +coverage=0 +debug=0 +recreate_db=1 +update=0 + +LANG=en_US.UTF-8 +LANGUAGE=en_US:en +LC_ALL=C + +process_options $@ +# Make our paths available to other scripts we call +export venv_path +export venv_dir +export venv_name +export tools_dir +export venv=${venv_path}/${venv_dir} + +if [ $no_site_packages -eq 1 ]; then + installvenvopts="--no-site-packages" +fi + + +function run_tests { + # Cleanup *pyc + ${wrapper} find . -type f -name "*.pyc" -delete + + if [ $debug -eq 1 ]; then + if [ "$testropts" = "" ] && [ "$testrargs" = "" ]; then + # Default to running all tests if specific test is not + # provided. + testrargs="discover ./gbp/" + fi + ${wrapper} python -m testtools.run $testropts $testrargs + + # Short circuit because all of the testr and coverage stuff + # below does not make sense when running testtools.run for + # debugging purposes. + return $? + fi + + if [ $coverage -eq 1 ]; then + TESTRTESTS="$TESTRTESTS --coverage" + else + TESTRTESTS="$TESTRTESTS --slowest" + fi + + # Just run the test suites in current environment + set +e + testrargs=`echo "$testrargs" | sed -e's/^\s*\(.*\)\s*$/\1/'` + TESTRTESTS="$TESTRTESTS --testr-args='--subunit $testropts $testrargs'" + OS_TEST_PATH=`echo $testrargs|grep -o 'gbp\.tests[^[:space:]:]*\+'|tr . /` + if [ -d "$OS_TEST_PATH" ]; then + wrapper="OS_TEST_PATH=$OS_TEST_PATH $wrapper" + elif [ -d "$(dirname $OS_TEST_PATH)" ]; then + wrapper="OS_TEST_PATH=$(dirname $OS_TEST_PATH) $wrapper" + fi + echo "Running \`${wrapper} $TESTRTESTS\`" + bash -c "${wrapper} $TESTRTESTS | ${wrapper} subunit2pyunit" + RESULT=$? + set -e + + copy_subunit_log + + if [ $coverage -eq 1 ]; then + echo "Generating coverage report in covhtml/" + # Don't compute coverage for common code, which is tested elsewhere + ${wrapper} coverage combine + ${wrapper} coverage html --include='gbp/*' --omit='gbp/openstack/common/*' -d covhtml -i + fi + + return $RESULT +} + +function copy_subunit_log { + LOGNAME=`cat .testrepository/next-stream` + LOGNAME=$(($LOGNAME - 1)) + LOGNAME=".testrepository/${LOGNAME}" + cp $LOGNAME subunit.log +} + +function run_pep8 { + echo "Running flake8 ..." + + ${wrapper} flake8 +} + + +TESTRTESTS="python setup.py testr" + +if [ $never_venv -eq 0 ] +then + # Remove the virtual environment if --force used + if [ $force -eq 1 ]; then + echo "Cleaning virtualenv..." + rm -rf ${venv} + fi + if [ $update -eq 1 ]; then + echo "Updating virtualenv..." + python tools/install_venv.py $installvenvopts + fi + if [ -e ${venv} ]; then + wrapper="${with_venv}" + else + if [ $always_venv -eq 1 ]; then + # Automatically install the virtualenv + python tools/install_venv.py $installvenvopts + wrapper="${with_venv}" + else + echo -e "No virtual environment found...create one? (Y/n) \c" + read use_ve + if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then + # Install the virtualenv and run the test suite in it + python tools/install_venv.py $installvenvopts + wrapper=${with_venv} + fi + fi + fi +fi + +# Delete old coverage data from previous runs +if [ $coverage -eq 1 ]; then + ${wrapper} coverage erase +fi + +if [ $just_pep8 -eq 1 ]; then + run_pep8 + exit +fi + +if [ $recreate_db -eq 1 ]; then + rm -f tests.sqlite +fi + +run_tests + +# NOTE(sirp): we only want to run pep8 when we're running the full-test suite, +# not when we're running tests individually. To handle this, we need to +# distinguish between options (testropts), which begin with a '-', and +# arguments (testrargs). +if [ -z "$testrargs" ]; then + if [ $no_pep8 -eq 0 ]; then + run_pep8 + fi +fi + diff --git a/setup.cfg b/setup.cfg index 9cc558e..096b7e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ setup-hooks = [entry_points] console_scripts = - gbp = gbpclient.shell:main + gbp = gbpclient.gbpshell:main [build_sphinx] all_files = 1 diff --git a/test-requirements.txt b/test-requirements.txt index 5db63d0..1f71a3c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,4 +15,5 @@ oslotest>=1.1.0.0a2 python-subunit>=0.0.18 sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 +testscenarios>=0.4 testtools>=0.9.34 diff --git a/tools/install_venv.py b/tools/install_venv.py new file mode 100644 index 0000000..cc21843 --- /dev/null +++ b/tools/install_venv.py @@ -0,0 +1,74 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2010 OpenStack Foundation +# Copyright 2013 IBM Corp. +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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 ConfigParser +import os +import sys + +import install_venv_common as install_venv # flake8: noqa + + +def print_help(project, venv, root): + help = """ + %(project)s development environment setup is complete. + + %(project)s development uses virtualenv to track and manage Python + dependencies while in development and testing. + + To activate the %(project)s virtualenv for the extent of your current + shell session you can run: + + $ source %(venv)s/bin/activate + + Or, if you prefer, you can run commands in the virtualenv on a case by + case basis by running: + + $ %(root)s/tools/with_venv.sh + """ + print help % dict(project=project, venv=venv, root=root) + + +def main(argv): + root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + + if os.environ.get('tools_path'): + root = os.environ['tools_path'] + venv = os.path.join(root, '.venv') + if os.environ.get('venv'): + venv = os.environ['venv'] + + pip_requires = os.path.join(root, 'requirements.txt') + test_requires = os.path.join(root, 'test-requirements.txt') + py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) + setup_cfg = ConfigParser.ConfigParser() + setup_cfg.read('setup.cfg') + project = setup_cfg.get('metadata', 'name') + + install = install_venv.InstallVenv( + root, venv, pip_requires, test_requires, py_version, project) + options = install.parse_args(argv) + install.check_python_version() + install.check_dependencies() + install.create_virtualenv(no_site_packages=options.no_site_packages) + install.install_dependencies() + print_help(project, venv, root) + +if __name__ == '__main__': + main(sys.argv) diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py new file mode 100644 index 0000000..e279159 --- /dev/null +++ b/tools/install_venv_common.py @@ -0,0 +1,172 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + +"""Provides methods needed by installation script for OpenStack development +virtual environments. + +Since this script is used to bootstrap a virtualenv from the system's Python +environment, it should be kept strictly compatible with Python 2.6. + +Synced in from openstack-common +""" + +from __future__ import print_function + +import optparse +import os +import subprocess +import sys + + +class InstallVenv(object): + + def __init__(self, root, venv, requirements, + test_requirements, py_version, + project): + self.root = root + self.venv = venv + self.requirements = requirements + self.test_requirements = test_requirements + self.py_version = py_version + self.project = project + + def die(self, message, *args): + print(message % args, file=sys.stderr) + sys.exit(1) + + def check_python_version(self): + if sys.version_info < (2, 6): + self.die("Need Python Version >= 2.6") + + def run_command_with_code(self, cmd, redirect_output=True, + check_exit_code=True): + """Runs a command in an out-of-process shell. + + Returns the output of that command. Working directory is self.root. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return (output, proc.returncode) + + def run_command(self, cmd, redirect_output=True, check_exit_code=True): + return self.run_command_with_code(cmd, redirect_output, + check_exit_code)[0] + + def get_distro(self): + if (os.path.exists('/etc/fedora-release') or + os.path.exists('/etc/redhat-release')): + return Fedora( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + else: + return Distro( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + + def check_dependencies(self): + self.get_distro().install_virtualenv() + + def create_virtualenv(self, no_site_packages=True): + """Creates the virtual environment and installs PIP. + + Creates the virtual environment and installs PIP only into the + virtual environment. + """ + if not os.path.isdir(self.venv): + print('Creating venv...', end=' ') + if no_site_packages: + self.run_command(['virtualenv', '-q', '--no-site-packages', + self.venv]) + else: + self.run_command(['virtualenv', '-q', self.venv]) + print('done.') + else: + print("venv already exists...") + pass + + def pip_install(self, *args): + self.run_command(['tools/with_venv.sh', + 'pip', 'install', '--upgrade'] + list(args), + redirect_output=False) + + def install_dependencies(self): + print('Installing dependencies with pip (this can take a while)...') + + # First things first, make sure our venv has the latest pip and + # setuptools and pbr + self.pip_install('pip>=1.4') + self.pip_install('setuptools') + self.pip_install('pbr') + + self.pip_install('-r', self.requirements, '-r', self.test_requirements) + + def parse_args(self, argv): + """Parses command-line arguments.""" + parser = optparse.OptionParser() + parser.add_option('-n', '--no-site-packages', + action='store_true', + help="Do not inherit packages from global Python " + "install.") + return parser.parse_args(argv[1:])[0] + + +class Distro(InstallVenv): + + def check_cmd(self, cmd): + return bool(self.run_command(['which', cmd], + check_exit_code=False).strip()) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if self.check_cmd('easy_install'): + print('Installing virtualenv via easy_install...', end=' ') + if self.run_command(['easy_install', 'virtualenv']): + print('Succeeded') + return + else: + print('Failed') + + self.die('ERROR: virtualenv not found.\n\n%s development' + ' requires virtualenv, please install it using your' + ' favorite package management tool' % self.project) + + +class Fedora(Distro): + """This covers all Fedora-based distributions. + + Includes: Fedora, RHEL, CentOS, Scientific Linux + """ + + def check_pkg(self, pkg): + return self.run_command_with_code(['rpm', '-q', pkg], + check_exit_code=False)[1] == 0 + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if not self.check_pkg('python-virtualenv'): + self.die("Please install 'python-virtualenv'.") + + super(Fedora, self).install_virtualenv() diff --git a/tools/policy.bash_completion b/tools/policy.bash_completion deleted file mode 100644 index dbfe187..0000000 --- a/tools/policy.bash_completion +++ /dev/null @@ -1,27 +0,0 @@ -_policy_opts="" # lazy init -_policy_flags="" # lazy init -_policy_opts_exp="" # lazy init -_policy() -{ - local cur prev nbc cflags - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - - if [ "x$_policy_opts" == "x" ] ; then - nbc="`policy bash-completion`" - _policy_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" - _policy_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" - _policy_opts_exp="`echo "$_policy_opts" | sed -e "s/\s/|/g"`" - fi - - if [[ " ${COMP_WORDS[@]} " =~ " "($_policy_opts_exp)" " && "$prev" != "help" ]] ; then - COMPLETION_CACHE=~/.policyclient/*/*-cache - cflags="$_policy_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') - COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) - else - COMPREPLY=($(compgen -W "${_policy_opts}" -- ${cur})) - fi - return 0 -} -complete -F _policy policy diff --git a/tools/with_venv.sh b/tools/with_venv.sh new file mode 100755 index 0000000..c8d2940 --- /dev/null +++ b/tools/with_venv.sh @@ -0,0 +1,4 @@ +#!/bin/bash +TOOLS=`dirname $0` +VENV=$TOOLS/../.venv +source $VENV/bin/activate && $@