diff options
Diffstat (limited to 'neutronclient')
24 files changed, 754 insertions, 58 deletions
diff --git a/neutronclient/client.py b/neutronclient/client.py index 0a68ff0..bc645f1 100644 --- a/neutronclient/client.py +++ b/neutronclient/client.py @@ -48,9 +48,6 @@ class HTTPClient(object): USER_AGENT = 'python-neutronclient' CONTENT_TYPE = 'application/json' - # 8192 Is the default max URI len for eventlet.wsgi.server - MAX_URI_LEN = 8192 - def __init__(self, username=None, user_id=None, tenant_name=None, tenant_id=None, password=None, auth_url=None, @@ -149,16 +146,9 @@ class HTTPClient(object): return resp, resp.text - def _check_uri_length(self, action): - uri_len = len(self.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, url, method, **kwargs): # Ensure client always has correct uri - do not guesstimate anything self.authenticate_and_fetch_endpoint_url() - self._check_uri_length(url) # Perform the request once. If we get a 401 back then it # might be because the auth token expired, so try to diff --git a/neutronclient/common/exceptions.py b/neutronclient/common/exceptions.py index 9728fbd..9a12f19 100644 --- a/neutronclient/common/exceptions.py +++ b/neutronclient/common/exceptions.py @@ -148,12 +148,6 @@ class OverQuotaClient(Conflict): pass -# TODO(amotoki): It is unused in Neutron, but it is referred to -# in Horizon code. After Horizon code is updated, remove it. -class AlreadyAttachedClient(Conflict): - pass - - class IpAddressGenerationFailureClient(Conflict): pass diff --git a/neutronclient/common/extension.py b/neutronclient/common/extension.py new file mode 100644 index 0000000..2ff8d34 --- /dev/null +++ b/neutronclient/common/extension.py @@ -0,0 +1,86 @@ +# Copyright 2015 Rackspace Hosting Inc. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from stevedore import extension + +from neutronclient.neutron import v2_0 as neutronV20 + + +def _discover_via_entry_points(): + emgr = extension.ExtensionManager('neutronclient.extension', + invoke_on_load=False) + return ((ext.name, ext.plugin) for ext in emgr) + + +class NeutronClientExtension(neutronV20.NeutronCommand): + pagination_support = False + _formatters = {} + sorting_support = False + + +class ClientExtensionShow(NeutronClientExtension, neutronV20.ShowCommand): + def get_data(self, parsed_args): + # NOTE(mdietz): Calls 'execute' to provide a consistent pattern + # for any implementers adding extensions with + # regard to any other extension verb. + return self.execute(parsed_args) + + def execute(self, parsed_args): + return super(ClientExtensionShow, self).get_data(parsed_args) + + +class ClientExtensionList(NeutronClientExtension, neutronV20.ListCommand): + + def get_data(self, parsed_args): + # NOTE(mdietz): Calls 'execute' to provide a consistent pattern + # for any implementers adding extensions with + # regard to any other extension verb. + return self.execute(parsed_args) + + def execute(self, parsed_args): + return super(ClientExtensionList, self).get_data(parsed_args) + + +class ClientExtensionDelete(NeutronClientExtension, neutronV20.DeleteCommand): + def run(self, parsed_args): + # NOTE(mdietz): Calls 'execute' to provide a consistent pattern + # for any implementers adding extensions with + # regard to any other extension verb. + return self.execute(parsed_args) + + def execute(self, parsed_args): + return super(ClientExtensionDelete, self).run(parsed_args) + + +class ClientExtensionCreate(NeutronClientExtension, neutronV20.CreateCommand): + def get_data(self, parsed_args): + # NOTE(mdietz): Calls 'execute' to provide a consistent pattern + # for any implementers adding extensions with + # regard to any other extension verb. + return self.execute(parsed_args) + + def execute(self, parsed_args): + return super(ClientExtensionCreate, self).get_data(parsed_args) + + +class ClientExtensionUpdate(NeutronClientExtension, neutronV20.UpdateCommand): + def run(self, parsed_args): + # NOTE(mdietz): Calls 'execute' to provide a consistent pattern + # for any implementers adding extensions with + # regard to any other extension verb. + return self.execute(parsed_args) + + def execute(self, parsed_args): + return super(ClientExtensionUpdate, self).run(parsed_args) diff --git a/neutronclient/neutron/v2_0/__init__.py b/neutronclient/neutron/v2_0/__init__.py index 1ca26e2..2fca3b3 100644 --- a/neutronclient/neutron/v2_0/__init__.py +++ b/neutronclient/neutron/v2_0/__init__.py @@ -492,9 +492,13 @@ class UpdateCommand(NeutronCommand): def get_parser(self, prog_name): parser = super(UpdateCommand, self).get_parser(prog_name) + if self.allow_names: + help_str = _('ID or name of %s to update.') + else: + help_str = _('ID of %s to update.') parser.add_argument( 'id', metavar=self.resource.upper(), - help=_('ID or name of %s to update.') % self.resource) + help=help_str % self.resource) self.add_known_arguments(parser) return parser diff --git a/neutronclient/neutron/v2_0/contrib/__init__.py b/neutronclient/neutron/v2_0/contrib/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/neutronclient/neutron/v2_0/contrib/__init__.py diff --git a/neutronclient/neutron/v2_0/contrib/_fox_sockets.py b/neutronclient/neutron/v2_0/contrib/_fox_sockets.py new file mode 100644 index 0000000..da88eb1 --- /dev/null +++ b/neutronclient/neutron/v2_0/contrib/_fox_sockets.py @@ -0,0 +1,85 @@ +# Copyright 2015 Rackspace Hosting Inc. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from neutronclient.common import extension +from neutronclient.i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 + + +def _add_updatable_args(parser): + parser.add_argument( + 'name', + help=_('Name of this fox socket.')) + + +def _updatable_args2body(parsed_args, body, client): + if parsed_args.name: + body['fox_socket'].update({'name': parsed_args.name}) + + +class FoxInSocket(extension.NeutronClientExtension): + resource = 'fox_socket' + resource_plural = '%ss' % resource + object_path = '/%s' % resource_plural + resource_path = '/%s/%%s' % resource_plural + versions = ['2.0'] + + +class FoxInSocketsList(extension.ClientExtensionList, FoxInSocket): + shell_command = 'fox-sockets-list' + list_columns = ['id', 'name'] + pagination_support = True + sorting_support = True + + +class FoxInSocketsCreate(extension.ClientExtensionCreate, FoxInSocket): + shell_command = 'fox-sockets-create' + list_columns = ['id', 'name'] + + def add_known_arguments(self, parser): + _add_updatable_args(parser) + + def args2body(self, parsed_args): + body = {'fox_socket': {}} + client = self.get_client() + _updatable_args2body(parsed_args, body, client) + neutronV20.update_dict(parsed_args, body['fox_socket'], []) + return body + + +class FoxInSocketsUpdate(extension.ClientExtensionUpdate, FoxInSocket): + shell_command = 'fox-sockets-update' + list_columns = ['id', 'name'] + + def add_known_arguments(self, parser): + #_add_updatable_args(parser) + parser.add_argument( + '--name', + help=_('Name of this fox socket.')) + + def args2body(self, parsed_args): + body = {'fox_socket': { + 'name': parsed_args.name}, } + neutronV20.update_dict(parsed_args, body['fox_socket'], []) + return body + + +class FoxInSocketsDelete(extension.ClientExtensionDelete, FoxInSocket): + shell_command = 'fox-sockets-delete' + + +class FoxInSocketsShow(extension.ClientExtensionShow, FoxInSocket): + shell_command = 'fox-sockets-show' diff --git a/neutronclient/neutron/v2_0/credential.py b/neutronclient/neutron/v2_0/credential.py index ffec5ee..c397627 100644 --- a/neutronclient/neutron/v2_0/credential.py +++ b/neutronclient/neutron/v2_0/credential.py @@ -32,7 +32,7 @@ class ShowCredential(neutronV20.ShowCommand): class CreateCredential(neutronV20.CreateCommand): - """Creates a credential.""" + """Create a credential.""" resource = 'credential' @@ -67,7 +67,7 @@ class CreateCredential(neutronV20.CreateCommand): class DeleteCredential(neutronV20.DeleteCommand): - """Delete a given credential.""" + """Delete a given credential.""" resource = 'credential' allow_names = False diff --git a/neutronclient/neutron/v2_0/lb/v2/healthmonitor.py b/neutronclient/neutron/v2_0/lb/v2/healthmonitor.py index 23136f7..ff1c11e 100644 --- a/neutronclient/neutron/v2_0/lb/v2/healthmonitor.py +++ b/neutronclient/neutron/v2_0/lb/v2/healthmonitor.py @@ -86,8 +86,15 @@ class CreateHealthMonitor(neutronV20.CreateCommand): '--type', required=True, choices=['PING', 'TCP', 'HTTP', 'HTTPS'], help=_('One of the predefined health monitor types.')) + parser.add_argument( + '--pool', required=True, + help=_('ID or name of the pool that this healthmonitor will ' + 'monitor.')) def args2body(self, parsed_args): + pool_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'pool', parsed_args.pool, + cmd_resource='lbaas_pool') body = { self.resource: { 'admin_state_up': parsed_args.admin_state, @@ -95,6 +102,7 @@ class CreateHealthMonitor(neutronV20.CreateCommand): 'max_retries': parsed_args.max_retries, 'timeout': parsed_args.timeout, 'type': parsed_args.type, + 'pool_id': pool_id }, } neutronV20.update_dict(parsed_args, body[self.resource], diff --git a/neutronclient/neutron/v2_0/lb/v2/listener.py b/neutronclient/neutron/v2_0/lb/v2/listener.py index 6b398ff..6ce9944 100644 --- a/neutronclient/neutron/v2_0/lb/v2/listener.py +++ b/neutronclient/neutron/v2_0/lb/v2/listener.py @@ -66,6 +66,15 @@ class CreateListener(neutronV20.CreateCommand): '--name', help=_('The name of the listener.')) parser.add_argument( + '--default-tls-container-id', + dest='default_tls_container_id', + help=_('Default TLS container ID to retrieve TLS information.')) + parser.add_argument( + '--sni-container-ids', + dest='sni_container_ids', + nargs='+', + help=_('List of TLS container IDs for SNI.')) + parser.add_argument( '--loadbalancer', required=True, metavar='LOADBALANCER', @@ -73,7 +82,7 @@ class CreateListener(neutronV20.CreateCommand): parser.add_argument( '--protocol', required=True, - choices=['TCP', 'HTTP', 'HTTPS'], + choices=['TCP', 'HTTP', 'HTTPS', 'TERMINATED_HTTPS'], help=_('Protocol for the listener.')) parser.add_argument( '--protocol-port', @@ -97,7 +106,9 @@ class CreateListener(neutronV20.CreateCommand): neutronV20.update_dict(parsed_args, body[self.resource], ['connection-limit', 'description', - 'loadbalancer_id', 'name']) + 'loadbalancer_id', 'name', + 'default_tls_container_id', + 'sni_container_ids']) return body diff --git a/neutronclient/neutron/v2_0/lb/v2/pool.py b/neutronclient/neutron/v2_0/lb/v2/pool.py index 11644be..7182171 100644 --- a/neutronclient/neutron/v2_0/lb/v2/pool.py +++ b/neutronclient/neutron/v2_0/lb/v2/pool.py @@ -76,12 +76,11 @@ class CreatePool(neutronV20.CreateCommand): '--description', help=_('Description of the pool.')) parser.add_argument( - '--healthmonitor-id', - help=_('ID of the health monitor to use.')) - parser.add_argument( '--session-persistence', metavar='TYPE:VALUE', help=_('The type of session persistence to use.')) parser.add_argument( + '--name', help=_('The name of the pool.')) + parser.add_argument( '--lb-algorithm', required=True, choices=['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'], @@ -96,9 +95,6 @@ class CreatePool(neutronV20.CreateCommand): required=True, choices=['HTTP', 'HTTPS', 'TCP'], help=_('Protocol for balancing.')) - parser.add_argument( - 'name', metavar='NAME', - help=_('The name of the pool.')) def args2body(self, parsed_args): if parsed_args.session_persistence: @@ -107,7 +103,6 @@ class CreatePool(neutronV20.CreateCommand): self.get_client(), 'listener', parsed_args.listener) body = { self.resource: { - 'name': parsed_args.name, 'admin_state_up': parsed_args.admin_state, 'protocol': parsed_args.protocol, 'lb_algorithm': parsed_args.lb_algorithm, @@ -115,7 +110,7 @@ class CreatePool(neutronV20.CreateCommand): }, } neutronV20.update_dict(parsed_args, body[self.resource], - ['description', 'healthmonitor_id', + ['description', 'name', 'session_persistence']) return body diff --git a/neutronclient/neutron/v2_0/networkprofile.py b/neutronclient/neutron/v2_0/networkprofile.py index 03fc497..3450b32 100644 --- a/neutronclient/neutron/v2_0/networkprofile.py +++ b/neutronclient/neutron/v2_0/networkprofile.py @@ -42,7 +42,7 @@ class ShowNetworkProfile(neutronV20.ShowCommand): class CreateNetworkProfile(neutronV20.CreateCommand): - """Creates a network profile.""" + """Create a network profile.""" resource = RESOURCE diff --git a/neutronclient/shell.py b/neutronclient/shell.py index 96f675c..177de52 100644 --- a/neutronclient/shell.py +++ b/neutronclient/shell.py @@ -22,6 +22,8 @@ from __future__ import print_function import argparse import getpass +import inspect +import itertools import logging import os import sys @@ -40,6 +42,7 @@ from cliff import commandmanager from neutronclient.common import clientmanager from neutronclient.common import command as openstack_command from neutronclient.common import exceptions as exc +from neutronclient.common import extension as client_extension from neutronclient.common import utils from neutronclient.i18n import _ from neutronclient.neutron.v2_0 import agent @@ -385,6 +388,8 @@ class NeutronShell(app.App): for k, v in self.commands[apiversion].items(): self.command_manager.add_command(k, v) + self._register_extensions(VERSION) + # Pop the 'complete' to correct the outputs of 'neutron help'. self.command_manager.commands.pop('complete') @@ -675,6 +680,25 @@ class NeutronShell(app.App): options.add(option) print(' '.join(commands | options)) + def _register_extensions(self, version): + for name, module in itertools.chain( + client_extension._discover_via_entry_points()): + self._extend_shell_commands(module, version) + + def _extend_shell_commands(self, module, version): + classes = inspect.getmembers(module, inspect.isclass) + for cls_name, cls in classes: + if (issubclass(cls, client_extension.NeutronClientExtension) and + hasattr(cls, 'shell_command')): + cmd = cls.shell_command + if hasattr(cls, 'versions'): + if version not in cls.versions: + continue + try: + self.command_manager.add_command(cmd, cls) + except TypeError: + pass + def run(self, argv): """Equivalent to the main program for the application. diff --git a/neutronclient/tests/functional/__init__.py b/neutronclient/tests/functional/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/neutronclient/tests/functional/__init__.py diff --git a/neutronclient/tests/functional/base.py b/neutronclient/tests/functional/base.py new file mode 100644 index 0000000..48561d6 --- /dev/null +++ b/neutronclient/tests/functional/base.py @@ -0,0 +1,44 @@ +# 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 os + +from tempest_lib.cli import base + + +class ClientTestBase(base.ClientTestBase): + """This is a first pass at a simple read only python-neutronclient test. + This only exercises client commands that are read only. + + This should test commands: + * as a regular user + * as a admin user + * with and without optional parameters + * initially just check return codes, and later test command outputs + + """ + + def _get_clients(self): + cli_dir = os.environ.get( + 'OS_NEUTRONCLIENT_EXEC_DIR', + os.path.join(os.path.abspath('.'), '.tox/functional/bin')) + + return base.CLIClient( + username=os.environ.get('OS_USERNAME'), + password=os.environ.get('OS_PASSWORD'), + tenant_name=os.environ.get('OS_TENANT_NAME'), + uri=os.environ.get('OS_AUTH_URL'), + cli_dir=cli_dir) + + def neutron(self, *args, **kwargs): + return self.clients.neutron(*args, + **kwargs) diff --git a/neutronclient/tests/functional/hooks/post_test_hook.sh b/neutronclient/tests/functional/hooks/post_test_hook.sh new file mode 100755 index 0000000..e0c9669 --- /dev/null +++ b/neutronclient/tests/functional/hooks/post_test_hook.sh @@ -0,0 +1,50 @@ +#!/bin/bash -xe + +# 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. + +# This script is executed inside post_test_hook function in devstack gate. + +function generate_testr_results { + if [ -f .testrepository/0 ]; then + sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit + sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit + sudo .tox/functional/bin/python /usr/local/jenkins/slave_scripts/subunit2html.py $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html + sudo gzip -9 $BASE/logs/testrepository.subunit + sudo gzip -9 $BASE/logs/testr_results.html + sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + fi +} + +export NEUTRONCLIENT_DIR="$BASE/new/python-neutronclient" + +# Get admin credentials +cd $BASE/new/devstack +source openrc admin admin + +# Go to the neutronclient dir +cd $NEUTRONCLIENT_DIR + +sudo chown -R jenkins:stack $NEUTRONCLIENT_DIR + +# Run tests +echo "Running neutronclient functional test suite" +set +e +# Preserve env for OS_ credentials +sudo -E -H -u jenkins tox -efunctional +EXIT_CODE=$? +set -e + +# Collect and parse result +generate_testr_results +exit $EXIT_CODE diff --git a/neutronclient/tests/functional/test_readonly_neutron.py b/neutronclient/tests/functional/test_readonly_neutron.py new file mode 100644 index 0000000..a552ef7 --- /dev/null +++ b/neutronclient/tests/functional/test_readonly_neutron.py @@ -0,0 +1,199 @@ +# 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 re + +from tempest_lib import exceptions + +from neutronclient.tests.functional import base + + +class SimpleReadOnlyNeutronClientTest(base.ClientTestBase): + + """This is a first pass at a simple read only python-neutronclient test. + This only exercises client commands that are read only. + + This should test commands: + * as a regular user + * as a admin user + * with and without optional parameters + * initially just check return codes, and later test command outputs + + """ + + def test_admin_fake_action(self): + self.assertRaises(exceptions.CommandFailed, + self.neutron, + 'this-does-neutron-exist') + + # NOTE(mestery): Commands in order listed in 'neutron help' + + # Optional arguments: + + def test_neutron_fake_action(self): + self.assertRaises(exceptions.CommandFailed, + self.neutron, + 'this-does-not-exist') + + def test_neutron_net_list(self): + net_list = self.parser.listing(self.neutron('net-list')) + self.assertTableStruct(net_list, ['id', 'name', 'subnets']) + + def test_neutron_ext_list(self): + ext = self.parser.listing(self.neutron('ext-list')) + self.assertTableStruct(ext, ['alias', 'name']) + + def test_neutron_dhcp_agent_list_hosting_net(self): + self.neutron('dhcp-agent-list-hosting-net', + params='private') + + def test_neutron_agent_list(self): + agents = self.parser.listing(self.neutron('agent-list')) + field_names = ['id', 'agent_type', 'host', 'alive', 'admin_state_up'] + self.assertTableStruct(agents, field_names) + + def test_neutron_floatingip_list(self): + self.neutron('floatingip-list') + + def test_neutron_meter_label_list(self): + self.neutron('meter-label-list') + + def test_neutron_meter_label_rule_list(self): + self.neutron('meter-label-rule-list') + + def _test_neutron_lbaas_command(self, command): + try: + self.neutron(command) + except exceptions.CommandFailed as e: + if '404 Not Found' not in e.stderr: + self.fail('%s: Unexpected failure.' % command) + + def test_neutron_lb_healthmonitor_list(self): + self._test_neutron_lbaas_command('lb-healthmonitor-list') + + def test_neutron_lb_member_list(self): + self._test_neutron_lbaas_command('lb-member-list') + + def test_neutron_lb_pool_list(self): + self._test_neutron_lbaas_command('lb-pool-list') + + def test_neutron_lb_vip_list(self): + self._test_neutron_lbaas_command('lb-vip-list') + + def test_neutron_net_external_list(self): + net_ext_list = self.parser.listing(self.neutron('net-external-list')) + self.assertTableStruct(net_ext_list, ['id', 'name', 'subnets']) + + def test_neutron_port_list(self): + port_list = self.parser.listing(self.neutron('port-list')) + self.assertTableStruct(port_list, ['id', 'name', 'mac_address', + 'fixed_ips']) + + def test_neutron_quota_list(self): + self.neutron('quota-list') + + def test_neutron_router_list(self): + router_list = self.parser.listing(self.neutron('router-list')) + self.assertTableStruct(router_list, ['id', 'name', + 'external_gateway_info']) + + def test_neutron_security_group_list(self): + security_grp = self.parser.listing(self.neutron('security-group-list')) + self.assertTableStruct(security_grp, ['id', 'name', 'description']) + + def test_neutron_security_group_rule_list(self): + security_grp = self.parser.listing(self.neutron + ('security-group-rule-list')) + self.assertTableStruct(security_grp, ['id', 'security_group', + 'direction', 'protocol', + 'remote_ip_prefix', + 'remote_group']) + + def test_neutron_subnet_list(self): + subnet_list = self.parser.listing(self.neutron('subnet-list')) + self.assertTableStruct(subnet_list, ['id', 'name', 'cidr', + 'allocation_pools']) + + def test_neutron_vpn_ikepolicy_list(self): + ikepolicy = self.parser.listing(self.neutron('vpn-ikepolicy-list')) + self.assertTableStruct(ikepolicy, ['id', 'name', + 'auth_algorithm', + 'encryption_algorithm', + 'ike_version', 'pfs']) + + def test_neutron_vpn_ipsecpolicy_list(self): + ipsecpolicy = self.parser.listing(self.neutron('vpn-ipsecpolicy-list')) + self.assertTableStruct(ipsecpolicy, ['id', 'name', + 'auth_algorithm', + 'encryption_algorithm', + 'pfs']) + + def test_neutron_vpn_service_list(self): + vpn_list = self.parser.listing(self.neutron('vpn-service-list')) + self.assertTableStruct(vpn_list, ['id', 'name', + 'router_id', 'status']) + + def test_neutron_ipsec_site_connection_list(self): + ipsec_site = self.parser.listing(self.neutron + ('ipsec-site-connection-list')) + self.assertTableStruct(ipsec_site, ['id', 'name', + 'peer_address', + 'peer_cidrs', + 'route_mode', + 'auth_mode', 'status']) + + def test_neutron_firewall_list(self): + firewall_list = self.parser.listing(self.neutron + ('firewall-list')) + self.assertTableStruct(firewall_list, ['id', 'name', + 'firewall_policy_id']) + + def test_neutron_firewall_policy_list(self): + firewall_policy = self.parser.listing(self.neutron + ('firewall-policy-list')) + self.assertTableStruct(firewall_policy, ['id', 'name', + 'firewall_rules']) + + def test_neutron_firewall_rule_list(self): + firewall_rule = self.parser.listing(self.neutron + ('firewall-rule-list')) + self.assertTableStruct(firewall_rule, ['id', 'name', + 'firewall_policy_id', + 'summary', 'enabled']) + + def test_neutron_help(self): + help_text = self.neutron('help') + lines = help_text.split('\n') + self.assertFirstLineStartsWith(lines, 'usage: neutron') + + commands = [] + cmds_start = lines.index('Commands for API v2.0:') + command_pattern = re.compile('^ {2}([a-z0-9\-\_]+)') + for line in lines[cmds_start:]: + match = command_pattern.match(line) + if match: + commands.append(match.group(1)) + commands = set(commands) + wanted_commands = set(('net-create', 'subnet-list', 'port-delete', + 'router-show', 'agent-update', 'help')) + self.assertFalse(wanted_commands - commands) + + # Optional arguments: + + def test_neutron_version(self): + self.neutron('', flags='--version') + + def test_neutron_debug_net_list(self): + self.neutron('net-list', flags='--debug') + + def test_neutron_quiet_net_list(self): + self.neutron('net-list', flags='--quiet') diff --git a/neutronclient/tests/unit/lb/v2/test_cli20_healthmonitor.py b/neutronclient/tests/unit/lb/v2/test_cli20_healthmonitor.py index 20af127..96f96a6 100644 --- a/neutronclient/tests/unit/lb/v2/test_cli20_healthmonitor.py +++ b/neutronclient/tests/unit/lb/v2/test_cli20_healthmonitor.py @@ -35,10 +35,11 @@ class CLITestV20LbHealthMonitorJSON(test_cli20.CLITestV20Base): max_retries = '3' delay = '10' timeout = '60' + pool = 'pool1' args = ['--type', type, '--max-retries', max_retries, - '--delay', delay, '--timeout', timeout] - position_names = ['type', 'max_retries', 'delay', 'timeout'] - position_values = [type, max_retries, delay, timeout] + '--delay', delay, '--timeout', timeout, '--pool', pool] + position_names = ['type', 'max_retries', 'delay', 'timeout', 'pool_id'] + position_values = [type, max_retries, delay, timeout, pool] self._test_create_resource(resource, cmd, '', my_id, args, position_names, position_values, cmd_resource=cmd_resource) @@ -57,15 +58,16 @@ class CLITestV20LbHealthMonitorJSON(test_cli20.CLITestV20Base): http_method = 'GET' expected_codes = '201' url_path = '/somepath' + pool = 'pool1' args = ['--admin-state-down', '--http-method', http_method, '--expected-codes', expected_codes, '--url-path', url_path, '--type', type, '--max-retries', max_retries, - '--delay', delay, '--timeout', timeout] + '--delay', delay, '--timeout', timeout, '--pool', pool] position_names = ['admin_state_up', 'http_method', 'expected_codes', 'url_path', 'type', 'max_retries', 'delay', - 'timeout'] + 'timeout', 'pool_id'] position_values = [False, http_method, expected_codes, url_path, - type, max_retries, delay, timeout] + type, max_retries, delay, timeout, pool] self._test_create_resource(resource, cmd, '', my_id, args, position_names, position_values, cmd_resource=cmd_resource) diff --git a/neutronclient/tests/unit/lb/v2/test_cli20_listener.py b/neutronclient/tests/unit/lb/v2/test_cli20_listener.py index fc0a062..ea421a8 100644 --- a/neutronclient/tests/unit/lb/v2/test_cli20_listener.py +++ b/neutronclient/tests/unit/lb/v2/test_cli20_listener.py @@ -51,12 +51,17 @@ class CLITestV20LbListenerJSON(test_cli20.CLITestV20Base): loadbalancer = 'loadbalancer' protocol = 'TCP' protocol_port = '80' + def_tls_cont_id = '11111' args = ['--admin-state-down', '--protocol', protocol, '--protocol-port', protocol_port, - '--loadbalancer', loadbalancer] + '--loadbalancer', loadbalancer, + '--default-tls-container-id', def_tls_cont_id, + '--sni-container-ids', '1111', '2222', '3333'] position_names = ['admin_state_up', - 'protocol', 'protocol_port', 'loadbalancer_id'] - position_values = [False, protocol, protocol_port, loadbalancer] + 'protocol', 'protocol_port', 'loadbalancer_id', + 'default_tls_container_id', 'sni_container_ids'] + position_values = [False, protocol, protocol_port, loadbalancer, + def_tls_cont_id, ['1111', '2222', '3333']] self._test_create_resource(resource, cmd, '', my_id, args, position_names, position_values, cmd_resource=cmd_resource) diff --git a/neutronclient/tests/unit/lb/v2/test_cli20_pool.py b/neutronclient/tests/unit/lb/v2/test_cli20_pool.py index daf3e52..77b5655 100644 --- a/neutronclient/tests/unit/lb/v2/test_cli20_pool.py +++ b/neutronclient/tests/unit/lb/v2/test_cli20_pool.py @@ -33,12 +33,11 @@ class CLITestV20LbPoolJSON(test_cli20.CLITestV20Base): lb_algorithm = 'ROUND_ROBIN' listener = 'listener' protocol = 'TCP' - name = 'my-pool' args = ['--lb-algorithm', lb_algorithm, '--protocol', protocol, - '--listener', listener, name] + '--listener', listener] position_names = ['admin_state_up', 'lb_algorithm', 'protocol', - 'listener_id', 'name'] - position_values = [True, lb_algorithm, protocol, listener, name] + 'listener_id'] + position_values = [True, lb_algorithm, protocol, listener] self._test_create_resource(resource, cmd, '', my_id, args, position_names, position_values, cmd_resource=cmd_resource) @@ -56,19 +55,16 @@ class CLITestV20LbPoolJSON(test_cli20.CLITestV20Base): session_persistence_str = 'HTTP_COOKIE:1234' session_persistence = {'type': 'HTTP_COOKIE', 'cookie_name': '1234'} - healthmon_id = 'healthmon-id' name = 'my-pool' args = ['--lb-algorithm', lb_algorithm, '--protocol', protocol, '--description', description, '--session-persistence', - session_persistence_str, '--healthmonitor-id', - healthmon_id, '--admin-state-down', name, + session_persistence_str, '--admin-state-down', '--name', name, '--listener', listener] position_names = ['lb_algorithm', 'protocol', 'description', - 'session_persistence', 'healthmonitor_id', - 'admin_state_up', 'listener_id', 'name'] + 'session_persistence', 'admin_state_up', 'name', + 'listener_id'] position_values = [lb_algorithm, protocol, description, - session_persistence, healthmon_id, - False, listener, name] + session_persistence, False, name, listener] self._test_create_resource(resource, cmd, '', my_id, args, position_names, position_values, cmd_resource=cmd_resource) diff --git a/neutronclient/tests/unit/test_cli20.py b/neutronclient/tests/unit/test_cli20.py index 5f5e06a..657b15e 100644 --- a/neutronclient/tests/unit/test_cli20.py +++ b/neutronclient/tests/unit/test_cli20.py @@ -91,10 +91,14 @@ class MyUrlComparator(mox.Comparator): lhsp = urlparse.urlparse(self.lhs) rhsp = urlparse.urlparse(rhs) + lhs_qs = urlparse.parse_qsl(lhsp.query) + rhs_qs = urlparse.parse_qsl(rhsp.query) + 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)) + len(lhs_qs) == len(rhs_qs) and + set(lhs_qs) == set(rhs_qs)) def __str__(self): if self.client and self.client.format != FORMAT: @@ -217,7 +221,8 @@ class CLITestV20Base(base.BaseTestCase): 'credential', 'network_profile', 'policy_profile', 'ikepolicy', 'ipsecpolicy', 'metering_label', - 'metering_label_rule', 'net_partition'] + 'metering_label_rule', 'net_partition', + 'fox_socket'] if not cmd_resource: cmd_resource = resource if (resource in non_admin_status_resources): @@ -617,6 +622,17 @@ class ClientV2TestJson(CLITestV20Base): self.mox.VerifyAll() self.mox.UnsetStubs() + def test_do_request_with_long_uri_exception(self): + long_string = 'x' * 8200 # 8200 > MAX_URI_LEN:8192 + params = {'id': long_string} + + try: + self.client.do_request('GET', '/test', body='', params=params) + except exceptions.RequestURITooLong as cm: + self.assertNotEqual(cm.excess, 0) + else: + self.fail('Expected exception NOT raised') + class ClientV2UnicodeTestXML(ClientV2TestJson): format = 'xml' diff --git a/neutronclient/tests/unit/test_cli20_network.py b/neutronclient/tests/unit/test_cli20_network.py index 1ac49b6..31754be 100644 --- a/neutronclient/tests/unit/test_cli20_network.py +++ b/neutronclient/tests/unit/test_cli20_network.py @@ -551,14 +551,14 @@ class CLITestV20NetworkJSON(test_cli20.CLITestV20Base): filters, response = self._build_test_data(data) # 1 char of extra URI len will cause a split in 2 requests - self.mox.StubOutWithMock(self.client.httpclient, + self.mox.StubOutWithMock(self.client, "_check_uri_length") - self.client.httpclient._check_uri_length(mox.IgnoreArg()).AndRaise( + self.client._check_uri_length(mox.IgnoreArg()).AndRaise( exceptions.RequestURITooLong(excess=1)) for data in sub_data_lists: filters, response = self._build_test_data(data) - self.client.httpclient._check_uri_length( + self.client._check_uri_length( mox.IgnoreArg()).AndReturn(None) self.client.httpclient.request( test_cli20.MyUrlComparator( diff --git a/neutronclient/tests/unit/test_cli20_securitygroup.py b/neutronclient/tests/unit/test_cli20_securitygroup.py index ec18fb8..8032e03 100644 --- a/neutronclient/tests/unit/test_cli20_securitygroup.py +++ b/neutronclient/tests/unit/test_cli20_securitygroup.py @@ -265,14 +265,14 @@ class CLITestV20SecurityGroupsJSON(test_cli20.CLITestV20Base): def test_extend_list_exceed_max_uri_len(self): def mox_calls(path, data): # 1 char of extra URI len will cause a split in 2 requests - self.mox.StubOutWithMock(self.client.httpclient, + self.mox.StubOutWithMock(self.client, '_check_uri_length') - self.client.httpclient._check_uri_length(mox.IgnoreArg()).AndRaise( + self.client._check_uri_length(mox.IgnoreArg()).AndRaise( exceptions.RequestURITooLong(excess=1)) responses = self._build_test_data(data, excess=1) for item in responses: - self.client.httpclient._check_uri_length( + self.client._check_uri_length( mox.IgnoreArg()).AndReturn(None) self.client.httpclient.request( test_cli20.end_url(path, item['filter']), diff --git a/neutronclient/tests/unit/test_client_extension.py b/neutronclient/tests/unit/test_client_extension.py new file mode 100644 index 0000000..fe1712b --- /dev/null +++ b/neutronclient/tests/unit/test_client_extension.py @@ -0,0 +1,87 @@ +# Copyright 2015 Rackspace Hosting Inc. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import sys + +import mock + +from neutronclient.neutron.v2_0.contrib import _fox_sockets as fox_sockets +from neutronclient.tests.unit import test_cli20 + + +class CLITestV20ExtensionJSON(test_cli20.CLITestV20Base): + def setUp(self): + # need to mock before super because extensions loaded on instantiation + self._mock_extension_loading() + super(CLITestV20ExtensionJSON, self).setUp(plurals={'tags': 'tag'}) + + def _create_patch(self, name, func=None): + patcher = mock.patch(name) + thing = patcher.start() + self.addCleanup(patcher.stop) + return thing + + def _mock_extension_loading(self): + ext_pkg = 'neutronclient.common.extension' + contrib = self._create_patch(ext_pkg + '._discover_via_entry_points') + iterator = iter([("_fox_sockets", fox_sockets)]) + contrib.return_value.__iter__.return_value = iterator + return contrib + + def test_delete_fox_socket(self): + """Delete fox socket: myid.""" + resource = 'fox_socket' + cmd = fox_sockets.FoxInSocketsDelete(test_cli20.MyApp(sys.stdout), + None) + myid = 'myid' + args = [myid] + self._test_delete_resource(resource, cmd, myid, args) + + def test_update_fox_socket(self): + """Update fox_socket: myid --name myname.""" + resource = 'fox_socket' + cmd = fox_sockets.FoxInSocketsUpdate(test_cli20.MyApp(sys.stdout), + None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname'], + {'name': 'myname'}) + + def test_create_fox_socket(self): + """Create fox_socket: myname.""" + resource = 'fox_socket' + cmd = fox_sockets.FoxInSocketsCreate(test_cli20.MyApp(sys.stdout), + None) + name = 'myname' + myid = 'myid' + args = [name, ] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_list_fox_sockets(self): + """List fox_sockets.""" + resources = 'fox_sockets' + cmd = fox_sockets.FoxInSocketsList(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_show_fox_socket(self): + """Show fox_socket: --fields id --fields name myid.""" + resource = 'fox_socket' + cmd = fox_sockets.FoxInSocketsShow(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']) diff --git a/neutronclient/v2_0/client.py b/neutronclient/v2_0/client.py index 87fca7c..6c430a9 100644 --- a/neutronclient/v2_0/client.py +++ b/neutronclient/v2_0/client.py @@ -15,6 +15,8 @@ # under the License. # +import inspect +import itertools import logging import time @@ -24,6 +26,7 @@ import six.moves.urllib.parse as urlparse from neutronclient import client from neutronclient.common import constants from neutronclient.common import exceptions +from neutronclient.common import extension as client_extension from neutronclient.common import serializer from neutronclient.common import utils from neutronclient.i18n import _ @@ -181,6 +184,12 @@ class ClientBase(object): # 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 @@ -189,6 +198,8 @@ class ClientBase(object): params = utils.safe_encode_dict(params) action += '?' + urlparse.urlencode(params, doseq=1) + self._check_uri_length(action) + if body: body = self.serialize(body) @@ -455,6 +466,39 @@ class Client(ClientBase): 'healthmonitors': 'healthmonitor', } + # 8192 Is the default max URI len for eventlet.wsgi.server + MAX_URI_LEN = 8192 + + @APIParamsCall + def list_ext(self, path, **_params): + """Client extension hook for lists. + """ + return self.get(path, params=_params) + + @APIParamsCall + def show_ext(self, path, id, **_params): + """Client extension hook for shows. + """ + return self.get(path % id, params=_params) + + @APIParamsCall + def create_ext(self, path, body=None): + """Client extension hook for creates. + """ + return self.post(path, body=body) + + @APIParamsCall + def update_ext(self, path, id, body=None): + """Client extension hook for updates. + """ + return self.put(path % id, body=body) + + @APIParamsCall + def delete_ext(self, path, id): + """Client extension hook for deletes. + """ + return self.delete(path % id) + @APIParamsCall def get_quotas_tenant(self, **_params): """Fetch tenant info in server's context for following quota operation. @@ -1538,3 +1582,59 @@ class Client(ClientBase): def delete_packet_filter(self, packet_filter_id): """Delete the specified packet filter.""" return self.delete(self.packet_filter_path % packet_filter_id) + + def __init__(self, **kwargs): + """Initialize a new client for the Neutron v2.0 API.""" + super(Client, self).__init__(**kwargs) + self._register_extensions(self.version) + + def extend_show(self, resource_plural, path): + def _fx(obj, **_params): + return self.show_ext(path, obj, **_params) + setattr(self, "show_%s" % resource_plural, _fx) + + def extend_list(self, resource_plural, path): + def _fx(**_params): + return self.list_ext(path, **_params) + setattr(self, "list_%s" % resource_plural, _fx) + + def extend_create(self, resource_singular, path): + def _fx(body=None): + return self.create_ext(path, body) + setattr(self, "create_%s" % resource_singular, _fx) + + def extend_delete(self, resource_singular, path): + def _fx(obj): + return self.delete_ext(path, obj) + setattr(self, "delete_%s" % resource_singular, _fx) + + def extend_update(self, resource_singular, path): + def _fx(obj, body=None): + return self.update_ext(path, obj, body) + setattr(self, "update_%s" % resource_singular, _fx) + + def _extend_client_with_module(self, module, version): + classes = inspect.getmembers(module, inspect.isclass) + for cls_name, cls in classes: + if hasattr(cls, 'versions'): + if version not in cls.versions: + continue + if issubclass(cls, client_extension.ClientExtensionList): + self.extend_list(cls.resource_plural, cls.object_path) + elif issubclass(cls, client_extension.ClientExtensionCreate): + self.extend_create(cls.resource, cls.object_path) + elif issubclass(cls, client_extension.ClientExtensionUpdate): + self.extend_update(cls.resource, cls.resource_path) + elif issubclass(cls, client_extension.ClientExtensionDelete): + self.extend_delete(cls.resource, cls.resource_path) + elif issubclass(cls, client_extension.ClientExtensionShow): + self.extend_show(cls.resource, cls.resource_path) + elif issubclass(cls, client_extension.NeutronClientExtension): + setattr(self, "%s_path" % cls.resource_plural, + cls.object_path) + setattr(self, "%s_path" % cls.resource, cls.resource_path) + + def _register_extensions(self, version): + for name, module in itertools.chain( + client_extension._discover_via_entry_points()): + self._extend_client_with_module(module, version) |