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 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.

View File

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

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.
#
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)

View File

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