diff --git a/neutronclient/common/extension.py b/neutronclient/common/extension.py new file mode 100644 index 000000000..2ff8d34a7 --- /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/contrib/__init__.py b/neutronclient/neutron/v2_0/contrib/__init__.py new file mode 100644 index 000000000..e69de29bb 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 000000000..da88eb124 --- /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/shell.py b/neutronclient/shell.py index 66fdca81a..4d7562e6f 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 @@ -381,6 +384,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') @@ -671,6 +676,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/unit/test_cli20.py b/neutronclient/tests/unit/test_cli20.py index 5f5e06a3e..86c79b417 100644 --- a/neutronclient/tests/unit/test_cli20.py +++ b/neutronclient/tests/unit/test_cli20.py @@ -217,7 +217,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): diff --git a/neutronclient/tests/unit/test_client_extension.py b/neutronclient/tests/unit/test_client_extension.py new file mode 100644 index 000000000..fe1712be7 --- /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 96405725b..0feac68e9 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 _ @@ -453,6 +456,36 @@ class Client(ClientBase): 'healthmonitors': 'healthmonitor', } + @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. @@ -1523,3 +1556,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) diff --git a/test-requirements.txt b/test-requirements.txt index 436948525..72514259e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,7 @@ coverage>=3.6 discover fixtures>=0.3.14 mox3>=0.7.0 +mock>=1.0 oslosphinx>=2.2.0 # Apache-2.0 oslotest>=1.2.0 # Apache-2.0 python-subunit>=0.0.18