Merge "Client command extension support"

This commit is contained in:
Jenkins
2015-02-25 18:03:22 +00:00
committed by Gerrit Code Review
8 changed files with 374 additions and 1 deletions

View File

@@ -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)

View File

@@ -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'

View File

@@ -22,6 +22,8 @@ from __future__ import print_function
import argparse import argparse
import getpass import getpass
import inspect
import itertools
import logging import logging
import os import os
import sys import sys
@@ -40,6 +42,7 @@ from cliff import commandmanager
from neutronclient.common import clientmanager from neutronclient.common import clientmanager
from neutronclient.common import command as openstack_command from neutronclient.common import command as openstack_command
from neutronclient.common import exceptions as exc from neutronclient.common import exceptions as exc
from neutronclient.common import extension as client_extension
from neutronclient.common import utils from neutronclient.common import utils
from neutronclient.i18n import _ from neutronclient.i18n import _
from neutronclient.neutron.v2_0 import agent from neutronclient.neutron.v2_0 import agent
@@ -381,6 +384,8 @@ class NeutronShell(app.App):
for k, v in self.commands[apiversion].items(): for k, v in self.commands[apiversion].items():
self.command_manager.add_command(k, v) self.command_manager.add_command(k, v)
self._register_extensions(VERSION)
# Pop the 'complete' to correct the outputs of 'neutron help'. # Pop the 'complete' to correct the outputs of 'neutron help'.
self.command_manager.commands.pop('complete') self.command_manager.commands.pop('complete')
@@ -671,6 +676,25 @@ class NeutronShell(app.App):
options.add(option) options.add(option)
print(' '.join(commands | options)) 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): def run(self, argv):
"""Equivalent to the main program for the application. """Equivalent to the main program for the application.

View File

@@ -217,7 +217,8 @@ class CLITestV20Base(base.BaseTestCase):
'credential', 'network_profile', 'credential', 'network_profile',
'policy_profile', 'ikepolicy', 'policy_profile', 'ikepolicy',
'ipsecpolicy', 'metering_label', 'ipsecpolicy', 'metering_label',
'metering_label_rule', 'net_partition'] 'metering_label_rule', 'net_partition',
'fox_socket']
if not cmd_resource: if not cmd_resource:
cmd_resource = resource cmd_resource = resource
if (resource in non_admin_status_resources): if (resource in non_admin_status_resources):

View File

@@ -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'])

View File

@@ -15,6 +15,8 @@
# under the License. # under the License.
# #
import inspect
import itertools
import logging import logging
import time import time
@@ -24,6 +26,7 @@ import six.moves.urllib.parse as urlparse
from neutronclient import client from neutronclient import client
from neutronclient.common import constants from neutronclient.common import constants
from neutronclient.common import exceptions from neutronclient.common import exceptions
from neutronclient.common import extension as client_extension
from neutronclient.common import serializer from neutronclient.common import serializer
from neutronclient.common import utils from neutronclient.common import utils
from neutronclient.i18n import _ from neutronclient.i18n import _
@@ -453,6 +456,36 @@ class Client(ClientBase):
'healthmonitors': 'healthmonitor', '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 @APIParamsCall
def get_quotas_tenant(self, **_params): def get_quotas_tenant(self, **_params):
"""Fetch tenant info in server's context for following quota operation. """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): def delete_packet_filter(self, packet_filter_id):
"""Delete the specified packet filter.""" """Delete the specified packet filter."""
return self.delete(self.packet_filter_path % packet_filter_id) 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)

View File

@@ -8,6 +8,7 @@ coverage>=3.6
discover discover
fixtures>=0.3.14 fixtures>=0.3.14
mox3>=0.7.0 mox3>=0.7.0
mock>=1.0
oslosphinx>=2.2.0 # Apache-2.0 oslosphinx>=2.2.0 # Apache-2.0
oslotest>=1.2.0 # Apache-2.0 oslotest>=1.2.0 # Apache-2.0
python-subunit>=0.0.18 python-subunit>=0.0.18