From d4a913044c34b414babe4487803a3ab1c50743d5 Mon Sep 17 00:00:00 2001 From: Jakub Ruzicka Date: Thu, 21 Feb 2013 14:36:19 +0100 Subject: [PATCH 01/29] Align with 2.1 pypi tarball. Change setup.cfg to match the one included in pypi tarball. --- setup.cfg | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index 394b128fe..6d19c21a9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,9 +4,11 @@ build-dir = doc/build source-dir = doc/source [nosetests] -# NOTE(jkoelker) To run the test suite under nose install the following -# coverage http://pypi.python.org/pypi/coverage -# tissue http://pypi.python.org/pypi/tissue (pep8 checker) -# openstack-nose https://github.com/jkoelker/openstack-nose -verbosity=2 -detailed-errors=1 +verbosity = 2 +detailed-errors = 1 + +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + From 562725209091bf2d929d199820a23d83088883f5 Mon Sep 17 00:00:00 2001 From: gongysh Date: Thu, 20 Sep 2012 06:46:32 +0800 Subject: [PATCH 02/29] clean the descriptions for quota cli commands. Bug #1053099 By default, the quotas for all tenants will use defaults from quantum.conf. Admin can use the cli commands to define different quotas for tenants. Change-Id: Ifb0ef3ce2680245ccb2c2872bed1cbef3bb9629b --- quantumclient/quantum/v2_0/quota.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quantumclient/quantum/v2_0/quota.py b/quantumclient/quantum/v2_0/quota.py index 4028bc720..2a6979c33 100644 --- a/quantumclient/quantum/v2_0/quota.py +++ b/quantumclient/quantum/v2_0/quota.py @@ -33,7 +33,7 @@ def get_tenant_id(tenant_id, client): class DeleteQuota(QuantumCommand): - """Delete a given tenant's quotas.""" + """Delete defined quotas of a given tenant.""" api = 'network' resource = 'quota' @@ -65,7 +65,7 @@ def run(self, parsed_args): class ListQuota(QuantumCommand, lister.Lister): - """List all tenants' quotas.""" + """List defined quotas of all tenants.""" api = 'network' resource = 'quota' @@ -95,7 +95,7 @@ def get_data(self, parsed_args): class ShowQuota(QuantumCommand, show.ShowOne): - """Show information of a given resource + """Show quotas of a given tenant """ api = 'network' @@ -142,7 +142,7 @@ def get_data(self, parsed_args): class UpdateQuota(QuantumCommand, show.ShowOne): - """Update port's information.""" + """Define tenant's quotas not to use defaults.""" resource = 'quota' log = logging.getLogger(__name__ + '.UpdateQuota') From 4ae542cc9858ea1dee0ade7a9f4c5961693e1ecc Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Mon, 22 Oct 2012 18:50:30 -0400 Subject: [PATCH 03/29] Add OpenStack trove classifier for PyPI Add trove classifier to have the client listed among the other OpenStack-related projets on PyPI. Change-Id: Ia37256fb8eb10e3e0e3943f1ccae4344a6ed97de Signed-off-by: Doug Hellmann --- setup.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/setup.py b/setup.py index 7681059b0..c3d851cbd 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,16 @@ description=ShortDescription, long_description=Description, license=License, + classifiers=[ + 'Environment :: OpenStack', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + ], scripts=ProjectScripts, dependency_links=dependency_links, install_requires=setup.parse_requirements(), From f745ed6d254a8438b967f04bded5baedeb7450d1 Mon Sep 17 00:00:00 2001 From: gongysh Date: Mon, 8 Oct 2012 18:48:26 +0800 Subject: [PATCH 04/29] Generate bash_completion string so that we can use bash completion. Bug #1063500 To install, copy tools/quantum.bash_completion to /etc/bash_completion.d/quantum Change-Id: I0afff3967c63111854455226fc90092f5bc7845a --- quantumclient/shell.py | 21 +++++++++++++++++++++ tools/quantum.bash_completion | 27 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 tools/quantum.bash_completion diff --git a/quantumclient/shell.py b/quantumclient/shell.py index 5f5eccdc8..c4195ba5c 100644 --- a/quantumclient/shell.py +++ b/quantumclient/shell.py @@ -278,6 +278,24 @@ def build_option_parser(self, description, version): return parser + def _bash_completion(self): + """ + Prints all of the commands and options to stdout so that the + quantum's bash-completion script doesn't have to hard code them. + """ + 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. @@ -289,6 +307,9 @@ def run(self, argv): command_pos = -1 help_pos = -1 for arg in argv: + if arg == 'bash-completion': + self._bash_completion() + return 0 if arg in COMMANDS[self.api_version]: if command_pos == -1: command_pos = index diff --git a/tools/quantum.bash_completion b/tools/quantum.bash_completion new file mode 100644 index 000000000..45db025af --- /dev/null +++ b/tools/quantum.bash_completion @@ -0,0 +1,27 @@ +_quantum_opts="" # lazy init +_quantum_flags="" # lazy init +_quantum_opts_exp="" # lazy init +_quantum() +{ + local cur prev nbc cflags + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + if [ "x$_quantum_opts" == "x" ] ; then + nbc="`quantum bash-completion | sed -e "s/\s-h\s/\s/"`" + _quantum_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _quantum_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _quantum_opts_exp="`echo "$_quantum_opts" | sed -e "s/\s/|/g"`" + fi + + if [[ " ${COMP_WORDS[@]} " =~ " "($_quantum_opts_exp)" " && "$prev" != "help" ]] ; then + COMPLETION_CACHE=~/.quantumclient/*/*-cache + cflags="$_quantum_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') + COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) + else + COMPREPLY=($(compgen -W "${_quantum_opts}" -- ${cur})) + fi + return 0 +} +complete -F _quantum quantum From a53cd46689e93a7d6fbe6cd732168ed02801f425 Mon Sep 17 00:00:00 2001 From: Alessandro Pilotti Date: Sat, 3 Nov 2012 21:40:50 +0200 Subject: [PATCH 05/29] Fixes setup compatibility issue on Windows Fixes Bug #1052161 "python setup.py build" fails on Windows due to a hardcoded shell path: /bin/sh setup.py updated using openstack-common/update.py Change-Id: I3943725a1f4b067a06f8f850e993c6867399a967 --- quantumclient/openstack/common/setup.py | 102 +++++++++++++++--------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/quantumclient/openstack/common/setup.py b/quantumclient/openstack/common/setup.py index caf06fa5b..e6f72f034 100644 --- a/quantumclient/openstack/common/setup.py +++ b/quantumclient/openstack/common/setup.py @@ -31,12 +31,13 @@ def parse_mailmap(mailmap='.mailmap'): mapping = {} if os.path.exists(mailmap): - fp = open(mailmap, 'r') - for l in fp: - l = l.strip() - if not l.startswith('#') and ' ' in l: - canonical_email, alias = l.split(' ') - mapping[alias] = canonical_email + with open(mailmap, 'r') as fp: + for l in fp: + l = l.strip() + if not l.startswith('#') and ' ' in l: + canonical_email, alias = [x for x in l.split(' ') + if x.startswith('<')] + mapping[alias] = canonical_email return mapping @@ -51,10 +52,10 @@ def canonicalize_emails(changelog, mapping): # Get requirements from the first file that exists def get_reqs_from_files(requirements_files): - reqs_in = [] for requirements_file in requirements_files: if os.path.exists(requirements_file): - return open(requirements_file, 'r').read().split('\n') + with open(requirements_file, 'r') as fil: + return fil.read().split('\n') return [] @@ -116,8 +117,12 @@ def write_requirements(): def _run_shell_command(cmd): - output = subprocess.Popen(["/bin/sh", "-c", cmd], - stdout=subprocess.PIPE) + if os.name == 'nt': + output = subprocess.Popen(["cmd.exe", "/C", cmd], + stdout=subprocess.PIPE) + else: + output = subprocess.Popen(["/bin/sh", "-c", cmd], + stdout=subprocess.PIPE) out = output.communicate() if len(out) == 0: return None @@ -135,11 +140,19 @@ def _get_git_next_version_suffix(branch_name): _run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*") milestone_cmd = "git show meta/openstack/release:%s" % branch_name milestonever = _run_shell_command(milestone_cmd) - if not milestonever: - milestonever = "" + if milestonever: + first_half = "%s~%s" % (milestonever, datestamp) + else: + first_half = datestamp + post_version = _get_git_post_version() - revno = post_version.split(".")[-1] - return "%s~%s.%s%s" % (milestonever, datestamp, revno_prefix, revno) + # post version should look like: + # 0.1.1.4.gcc9e28a + # where the bit after the last . is the short sha, and the bit between + # the last and second to last is the revno count + (revno, sha) = post_version.split(".")[-2:] + second_half = "%s%s.%s" % (revno_prefix, revno, sha) + return ".".join((first_half, second_half)) def _get_git_current_tag(): @@ -161,39 +174,48 @@ def _get_git_post_version(): cmd = "git --no-pager log --oneline" out = _run_shell_command(cmd) revno = len(out.split("\n")) + sha = _run_shell_command("git describe --always") else: tag_infos = tag_info.split("-") base_version = "-".join(tag_infos[:-2]) - revno = tag_infos[-2] - return "%s.%s" % (base_version, revno) + (revno, sha) = tag_infos[-2:] + return "%s.%s.%s" % (base_version, revno, sha) def write_git_changelog(): """Write a changelog based on the git changelog.""" - if os.path.isdir('.git'): - git_log_cmd = 'git log --stat' - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open("ChangeLog", "w") as changelog_file: - changelog_file.write(canonicalize_emails(changelog, mailmap)) + new_changelog = 'ChangeLog' + if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): + if os.path.isdir('.git'): + git_log_cmd = 'git log --stat' + changelog = _run_shell_command(git_log_cmd) + mailmap = parse_mailmap() + with open(new_changelog, "w") as changelog_file: + changelog_file.write(canonicalize_emails(changelog, mailmap)) + else: + open(new_changelog, 'w').close() def generate_authors(): """Create AUTHORS file using git commits.""" - jenkins_email = 'jenkins@review.openstack.org' + jenkins_email = 'jenkins@review.(openstack|stackforge).org' old_authors = 'AUTHORS.in' new_authors = 'AUTHORS' - if os.path.isdir('.git'): - # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | " - "grep -v " + jenkins_email) - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open(new_authors, 'w') as new_authors_fh: - new_authors_fh.write(canonicalize_emails(changelog, mailmap)) - if os.path.exists(old_authors): - with open(old_authors, "r") as old_authors_fh: - new_authors_fh.write('\n' + old_authors_fh.read()) + if not os.getenv('SKIP_GENERATE_AUTHORS'): + if os.path.isdir('.git'): + # don't include jenkins email address in AUTHORS file + git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | " + "egrep -v '" + jenkins_email + "'") + changelog = _run_shell_command(git_log_cmd) + mailmap = parse_mailmap() + with open(new_authors, 'w') as new_authors_fh: + new_authors_fh.write(canonicalize_emails(changelog, mailmap)) + if os.path.exists(old_authors): + with open(old_authors, "r") as old_authors_fh: + new_authors_fh.write('\n' + old_authors_fh.read()) + else: + open(new_authors, 'w').close() + _rst_template = """%(heading)s %(underline)s @@ -207,7 +229,7 @@ def generate_authors(): def read_versioninfo(project): """Read the versioninfo file. If it doesn't exist, we're in a github - zipball, and there's really know way to know what version we really + zipball, and there's really no way to know what version we really are, but that should be ok, because the utility of that should be just about nil if this code path is in use in the first place.""" versioninfo_path = os.path.join(project, 'versioninfo') @@ -221,7 +243,8 @@ def read_versioninfo(project): def write_versioninfo(project, version): """Write a simple file containing the version of the package.""" - open(os.path.join(project, 'versioninfo'), 'w').write("%s\n" % version) + with open(os.path.join(project, 'versioninfo'), 'w') as fil: + fil.write("%s\n" % version) def get_cmdclass(): @@ -312,7 +335,8 @@ def get_git_branchname(): def get_pre_version(projectname, base_version): - """Return a version which is based""" + """Return a version which is leading up to a version that will + be released in the future.""" if os.path.isdir('.git'): current_tag = _get_git_current_tag() if current_tag is not None: @@ -324,10 +348,10 @@ def get_pre_version(projectname, base_version): version_suffix = _get_git_next_version_suffix(branch_name) version = "%s~%s" % (base_version, version_suffix) write_versioninfo(projectname, version) - return version.split('~')[0] + return version else: version = read_versioninfo(projectname) - return version.split('~')[0] + return version def get_post_version(projectname): From 7146e402f185a6a341934abd7db7cd56019b3a44 Mon Sep 17 00:00:00 2001 From: gongysh Date: Fri, 9 Nov 2012 19:36:30 +0800 Subject: [PATCH 06/29] Fix curl command for PUT and DELETE. Bug #1076968 We print request curl command before HTTP request, and response information after HTTP request. Change-Id: I19840e8bd606a30389cd029c0add066c945fcdf6 --- quantumclient/client.py | 5 ++--- quantumclient/common/utils.py | 40 +++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/quantumclient/client.py b/quantumclient/client.py index 18bb3de2a..39e770e79 100644 --- a/quantumclient/client.py +++ b/quantumclient/client.py @@ -123,10 +123,9 @@ def _cs_request(self, *args, **kwargs): if 'body' in kwargs: kargs['body'] = kwargs['body'] - + utils.http_log_req(_logger, args, kargs) resp, body = self.request(*args, **kargs) - - utils.http_log(_logger, args, kargs, resp, body) + utils.http_log_resp(_logger, resp, body) status_code = self.get_status_code(resp) if status_code == 401: raise exceptions.Unauthorized(message=body) diff --git a/quantumclient/common/utils.py b/quantumclient/common/utils.py index bed02e010..45c64a998 100644 --- a/quantumclient/common/utils.py +++ b/quantumclient/common/utils.py @@ -148,23 +148,27 @@ def str2dict(strdict): return _info -def http_log(_logger, args, kwargs, resp, body): - if not _logger.isEnabledFor(logging.DEBUG): - return - - string_parts = ['curl -i'] - for element in args: - if element in ('GET', 'POST'): - string_parts.append(' -X %s' % element) - else: - string_parts.append(' %s' % element) +def http_log_req(_logger, args, kwargs): + if not _logger.isEnabledFor(logging.DEBUG): + return + + string_parts = ['curl -i'] + for element in args: + if element in ('GET', 'POST', 'DELETE', 'PUT'): + string_parts.append(' -X %s' % element) + else: + string_parts.append(' %s' % element) + + for element in kwargs['headers']: + header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) + string_parts.append(header) + + if 'body' in kwargs and kwargs['body']: + string_parts.append(" -d '%s'" % (kwargs['body'])) + _logger.debug("\nREQ: %s\n" % "".join(string_parts)) - for element in kwargs['headers']: - header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) - string_parts.append(header) - _logger.debug("REQ: %s\n" % "".join(string_parts)) - if 'body' in kwargs and kwargs['body']: - _logger.debug("REQ BODY: %s\n" % (kwargs['body'])) - _logger.debug("RESP:%s\n", resp) - _logger.debug("RESP BODY:%s\n", body) +def http_log_resp(_logger, resp, body): + if not _logger.isEnabledFor(logging.DEBUG): + return + _logger.debug("RESP:%s %s\n", resp, body) From 77ecd23a6b3336ba602fb0a61c3a3568bbc4e238 Mon Sep 17 00:00:00 2001 From: ivan-zhu Date: Wed, 10 Oct 2012 17:24:35 +0800 Subject: [PATCH 07/29] Convenience cmds for l3 Bug #1049551 Add two CLI and unit tests: quantum net-external-list (runs net-list with router:external=True filter) quantum router-port-list (runs port-list, filtering with device_id equal to specified router) Change-Id: I9a9668836ac24d4cbc6a3867ec031611b64ded14 --- quantumclient/quantum/v2_0/network.py | 15 ++ quantumclient/quantum/v2_0/port.py | 29 +++- quantumclient/shell.py | 4 + .../tests/unit/test_cli20_network.py | 160 ++++++++++++++++-- quantumclient/tests/unit/test_cli20_port.py | 127 ++++++++++++-- 5 files changed, 302 insertions(+), 33 deletions(-) diff --git a/quantumclient/quantum/v2_0/network.py b/quantumclient/quantum/v2_0/network.py index 21efd2d84..070de54c4 100644 --- a/quantumclient/quantum/v2_0/network.py +++ b/quantumclient/quantum/v2_0/network.py @@ -41,6 +41,21 @@ class ListNetwork(ListCommand): list_columns = ['id', 'name', 'subnets'] +class ListExternalNetwork(ListCommand): + """List external networks that belong to a given tenant""" + + resource = 'network' + log = logging.getLogger(__name__ + '.ListExternalNetwork') + _formatters = {'subnets': _format_subnets, } + list_colums = ['id', 'name', 'subnets'] + + def get_data(self, parsed_args): + if '--' not in parsed_args.filter_specs: + parsed_args.filter_specs.append('--') + parsed_args.filter_specs.append('--router:external=True') + return super(ListExternalNetwork, self).get_data(parsed_args) + + class ShowNetwork(ShowCommand): """Show information of a given network.""" diff --git a/quantumclient/quantum/v2_0/port.py b/quantumclient/quantum/v2_0/port.py index 6984f5de5..7f393654a 100644 --- a/quantumclient/quantum/v2_0/port.py +++ b/quantumclient/quantum/v2_0/port.py @@ -35,7 +35,7 @@ def _format_fixed_ips(port): class ListPort(ListCommand): - """List networks that belong to a given tenant.""" + """List ports that belong to a given tenant.""" resource = 'port' log = logging.getLogger(__name__ + '.ListPort') @@ -43,6 +43,33 @@ class ListPort(ListCommand): list_columns = ['id', 'name', 'mac_address', 'fixed_ips'] +class ListRouterPort(ListCommand): + """List ports that belong to a given tenant, with specified router""" + + resource = 'port' + log = logging.getLogger(__name__ + '.ListRouterPort') + _formatters = {'fixed_ips': _format_fixed_ips, } + list_columns = ['id', 'name', 'mac_address', 'fixed_ips'] + + def get_parser(self, prog_name): + parser = super(ListCommand, self).get_parser(prog_name) + quantumv20.add_show_list_common_argument(parser) + parser.add_argument( + 'id', metavar='router', + help='ID or name of router to look up') + quantumv20.add_extra_argument(parser, 'filter_specs', + 'filters options') + return parser + + def get_data(self, parsed_args): + quantum_client = self.get_client() + quantum_client.format = parsed_args.request_format + _id = quantumv20.find_resourceid_by_name_or_id( + quantum_client, 'router', parsed_args.id) + parsed_args.filter_specs.append('--device_id=%s' % _id) + return super(ListRouterPort, self).get_data(parsed_args) + + class ShowPort(ShowCommand): """Show information of a given port.""" diff --git a/quantumclient/shell.py b/quantumclient/shell.py index c4195ba5c..34f1a9992 100644 --- a/quantumclient/shell.py +++ b/quantumclient/shell.py @@ -54,6 +54,8 @@ def env(*_vars, **kwargs): COMMAND_V2 = { 'net-list': utils.import_class( 'quantumclient.quantum.v2_0.network.ListNetwork'), + 'net-external-list': utils.import_class( + 'quantumclient.quantum.v2_0.network.ListExternalNetwork'), 'net-show': utils.import_class( 'quantumclient.quantum.v2_0.network.ShowNetwork'), 'net-create': utils.import_class( @@ -96,6 +98,8 @@ def env(*_vars, **kwargs): 'quantumclient.quantum.v2_0.extension.ShowExt'), 'router-list': utils.import_class( 'quantumclient.quantum.v2_0.router.ListRouter'), + 'router-port-list': utils.import_class( + 'quantumclient.quantum.v2_0.port.ListRouterPort'), 'router-show': utils.import_class( 'quantumclient.quantum.v2_0.router.ShowRouter'), 'router-create': utils.import_class( diff --git a/quantumclient/tests/unit/test_cli20_network.py b/quantumclient/tests/unit/test_cli20_network.py index 729e98274..5360c08cc 100644 --- a/quantumclient/tests/unit/test_cli20_network.py +++ b/quantumclient/tests/unit/test_cli20_network.py @@ -16,6 +16,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 import sys +from mox import ContainsKeyValue from quantumclient.common import exceptions from quantumclient.common import utils @@ -27,6 +28,7 @@ from quantumclient.quantum.v2_0.network import UpdateNetwork from quantumclient.quantum.v2_0.network import ShowNetwork from quantumclient.quantum.v2_0.network import DeleteNetwork +from quantumclient.quantum.v2_0.network import ListExternalNetwork class CLITestV20Network(CLITestV20Base): @@ -39,8 +41,8 @@ def test_create_network(self): args = [name, ] position_names = ['name', ] position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) def test_create_network_tenant(self): """Create net: --tenant_id tenantid myname.""" @@ -51,15 +53,15 @@ def test_create_network_tenant(self): args = ['--tenant_id', 'tenantid', name] position_names = ['name', ] position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') # Test dashed options args = ['--tenant-id', 'tenantid', name] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') def test_create_network_tags(self): """Create net: myname --tags a b.""" @@ -70,9 +72,9 @@ def test_create_network_tags(self): args = [name, '--tags', 'a', 'b'] position_names = ['name', ] position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tags=['a', 'b']) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tags=['a', 'b']) def test_create_network_state(self): """Create net: --admin_state_down myname.""" @@ -83,15 +85,15 @@ def test_create_network_state(self): args = ['--admin_state_down', name, ] position_names = ['name', ] position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - admin_state_up=False) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + admin_state_up=False) # Test dashed options args = ['--admin-state-down', name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - admin_state_up=False) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + admin_state_up=False) def test_list_nets_empty_with_column(self): resources = "networks" @@ -179,6 +181,130 @@ def test_list_nets_with_default_column(self): self.assertEquals(0, len(set(network) ^ set(cmd.list_columns))) + def test_list_external_nets_empty_with_column(self): + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: []} + resstr = self.client.serialize(reses) + # url method body + query = "router%3Aexternal=True&id=myfakeid" + args = ['-c', 'id', '--', '--id', 'myfakeid'] + path = getattr(self.client, resources + "_path") + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=test_cli20.ContainsKeyValue( + 'X-Auth-Token', + test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + + parsed_args = cmd_parser.parse_args(args) + + cmd.run(parsed_args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertEquals('\n', _str) + + def _test_list_external_nets(self, resources, cmd, + detail=False, tags=[], + fields_1=[], fields_2=[]): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: [{'id': 'myid1', }, + {'id': 'myid2', }, ], } + + resstr = self.client.serialize(reses) + + # url method body + query = "" + args = detail and ['-D', ] or [] + 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 (not tags) and fields_2: + args.append('--') + if fields_2: + args.append("--fields") + for field in fields_2: + args.append(field) + fields_1.extend(fields_2) + for field in fields_1: + if query: + query += "&fields=" + field + else: + query = "fields=" + field + if query: + query += '&router%3Aexternal=True' + else: + query += 'router%3Aexternal=True' + for tag in tags: + if query: + query += "&tag=" + tag + else: + query = "tag=" + tag + if detail: + query = query and query + '&verbose=True' or 'verbose=True' + path = getattr(self.client, resources + "_path") + + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=ContainsKeyValue('X-Auth-Token', + test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + + parsed_args = cmd_parser.parse_args(args) + cmd.run(parsed_args) + + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + + self.assertTrue('myid1' in _str) + + def test_list_external_nets_detail(self): + """list external nets: -D.""" + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self._test_list_external_nets(resources, cmd, True) + + def test_list_external_nets_tags(self): + """List external nets: -- --tags a b.""" + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self._test_list_external_nets(resources, + cmd, tags=['a', 'b']) + + def test_list_external_nets_detail_tags(self): + """List external nets: -D -- --tags a b.""" + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self._test_list_external_nets(resources, cmd, + detail=True, tags=['a', 'b']) + + def test_list_externel_nets_fields(self): + """List external nets: --fields a --fields b -- --fields c d.""" + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self._test_list_external_nets(resources, cmd, + fields_1=['a', 'b'], + fields_2=['c', 'd']) + def test_update_network_exception(self): """Update net: myid.""" resource = 'network' diff --git a/quantumclient/tests/unit/test_cli20_port.py b/quantumclient/tests/unit/test_cli20_port.py index c57aa899d..6ddbf319c 100644 --- a/quantumclient/tests/unit/test_cli20_port.py +++ b/quantumclient/tests/unit/test_cli20_port.py @@ -16,14 +16,17 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 import sys +from mox import ContainsKeyValue from quantumclient.tests.unit.test_cli20 import CLITestV20Base from quantumclient.tests.unit.test_cli20 import MyApp +from quantumclient.tests.unit import test_cli20 from quantumclient.quantum.v2_0.port import CreatePort from quantumclient.quantum.v2_0.port import ListPort from quantumclient.quantum.v2_0.port import UpdatePort from quantumclient.quantum.v2_0.port import ShowPort from quantumclient.quantum.v2_0.port import DeletePort +from quantumclient.quantum.v2_0.port import ListRouterPort class CLITestV20Port(CLITestV20Base): @@ -39,8 +42,8 @@ def test_create_port(self): position_names = ['network_id'] position_values = [] position_values.extend([netid]) - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) def test_create_port_full(self): """Create port: --mac_address mac --device_id deviceid netid.""" @@ -52,14 +55,14 @@ def test_create_port_full(self): args = ['--mac_address', 'mac', '--device_id', 'deviceid', netid] position_names = ['network_id', 'mac_address', 'device_id'] position_values = [netid, 'mac', 'deviceid'] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) # Test dashed options args = ['--mac-address', 'mac', '--device-id', 'deviceid', netid] position_names = ['network_id', 'mac_address', 'device_id'] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) def test_create_port_tenant(self): """Create port: --tenant_id tenantid netid.""" @@ -72,15 +75,15 @@ def test_create_port_tenant(self): position_names = ['network_id'] position_values = [] position_values.extend([netid]) - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') # Test dashed options args = ['--tenant-id', 'tenantid', netid, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') def test_create_port_tags(self): """Create port: netid mac_address device_id --tags a b.""" @@ -93,9 +96,9 @@ def test_create_port_tags(self): position_names = ['network_id'] position_values = [] position_values.extend([netid]) - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tags=['a', 'b']) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tags=['a', 'b']) def test_list_ports(self): """List ports: -D.""" @@ -122,6 +125,100 @@ def test_list_ports_fields(self): self._test_list_resources(resources, cmd, fields_1=['a', 'b'], fields_2=['c', 'd']) + def _test_list_router_port(self, resources, cmd, + myid, detail=False, tags=[], + fields_1=[], fields_2=[]): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: [{'id': 'myid1', }, + {'id': 'myid2', }, ], } + + resstr = self.client.serialize(reses) + + # url method body + query = "" + args = detail and ['-D', ] or [] + + if fields_1: + for field in fields_1: + args.append('--fields') + args.append(field) + args.append(myid) + if tags: + args.append('--') + args.append("--tag") + for tag in tags: + args.append(tag) + if (not tags) and fields_2: + args.append('--') + if fields_2: + args.append("--fields") + for field in fields_2: + args.append(field) + fields_1.extend(fields_2) + for field in fields_1: + if query: + query += "&fields=" + field + else: + query = "fields=" + field + + for tag in tags: + if query: + query += "&tag=" + tag + else: + query = "tag=" + tag + if detail: + query = query and query + '&verbose=True' or 'verbose=True' + query = query and query + '&device_id=%s' or 'device_id=%s' + path = getattr(self.client, resources + "_path") + self.client.httpclient.request( + test_cli20.end_url(path, query % myid), 'GET', + body=None, + headers=ContainsKeyValue('X-Auth-Token', + test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + + parsed_args = cmd_parser.parse_args(args) + cmd.run(parsed_args) + + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + + self.assertTrue('myid1' in _str) + + def test_list_router_ports(self): + """List router ports: -D.""" + resources = "ports" + cmd = ListRouterPort(MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, + self.test_id, True) + + def test_list_router_ports_tags(self): + """List router ports: -- --tags a b.""" + resources = "ports" + cmd = ListRouterPort(MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, + self.test_id, tags=['a', 'b']) + + def test_list_router_ports_detail_tags(self): + """List router ports: -D -- --tags a b.""" + resources = "ports" + cmd = ListRouterPort(MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, self.test_id, + detail=True, tags=['a', 'b']) + + def test_list_router_ports_fields(self): + """List ports: --fields a --fields b -- --fields c d.""" + resources = "ports" + cmd = ListRouterPort(MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, self.test_id, + fields_1=['a', 'b'], + fields_2=['c', 'd']) + def test_update_port(self): """Update port: myid --name myname --tags a b.""" resource = 'port' From 5dff8c653031954a75f531c2459220abd2ec7d10 Mon Sep 17 00:00:00 2001 From: Kyle Mestery Date: Thu, 1 Nov 2012 15:53:14 +0000 Subject: [PATCH 08/29] Correct the verbose output formatting when creating routers. fixes bug 1070460 Change-Id: Id9157e7226f906ec608aa95766eeed1e62b5cc50 --- quantumclient/quantum/v2_0/__init__.py | 48 ++++++++++---------------- quantumclient/quantum/v2_0/router.py | 1 + 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/quantumclient/quantum/v2_0/__init__.py b/quantumclient/quantum/v2_0/__init__.py index c719d15ba..b067d32d4 100644 --- a/quantumclient/quantum/v2_0/__init__.py +++ b/quantumclient/quantum/v2_0/__init__.py @@ -200,6 +200,20 @@ def get_parser(self, prog_name): return parser + def format_output_data(self, data): + # Modify data to make it more readable + if self.resource in data: + for k, v in data[self.resource].iteritems(): + if isinstance(v, list): + value = '\n'.join(utils.dumps(i) if isinstance(i, dict) + else str(i) for i in v) + data[self.resource][k] = value + elif isinstance(v, dict): + value = utils.dumps(v) + data[self.resource][k] = value + elif v is None: + data[self.resource][k] = '' + class CreateCommand(QuantumCommand, show.ShowOne): """Create a resource for a given tenant @@ -239,25 +253,13 @@ def get_data(self, parsed_args): obj_creator = getattr(quantum_client, "create_%s" % self.resource) data = obj_creator(body) + self.format_output_data(data) # {u'network': {u'id': u'e9424a76-6db4-4c93-97b6-ec311cd51f19'}} info = self.resource in data and data[self.resource] or None if info: print >>self.app.stdout, _('Created a new %s:' % self.resource) else: info = {'': ''} - for k, v in info.iteritems(): - if isinstance(v, list): - value = "" - for _item in v: - if value: - value += "\n" - if isinstance(_item, dict): - value += utils.dumps(_item) - else: - value += str(_item) - info[k] = value - elif v is None: - info[k] = '' return zip(*sorted(info.iteritems())) @@ -435,23 +437,9 @@ def get_data(self, parsed_args): obj_shower = getattr(quantum_client, "show_%s" % self.resource) data = obj_shower(_id, **params) + self.format_output_data(data) + resource = data[self.resource] if self.resource in data: - for k, v in data[self.resource].iteritems(): - if isinstance(v, list): - value = "" - for _item in v: - if value: - value += "\n" - if isinstance(_item, dict): - value += utils.dumps(_item) - else: - value += str(_item) - data[self.resource][k] = value - elif isinstance(v, dict): - value = utils.dumps(v) - data[self.resource][k] = value - elif v is None: - data[self.resource][k] = '' - return zip(*sorted(data[self.resource].iteritems())) + return zip(*sorted(resource.iteritems())) else: return None diff --git a/quantumclient/quantum/v2_0/router.py b/quantumclient/quantum/v2_0/router.py index 9d6787d6a..db85b8057 100644 --- a/quantumclient/quantum/v2_0/router.py +++ b/quantumclient/quantum/v2_0/router.py @@ -56,6 +56,7 @@ class CreateRouter(CreateCommand): resource = 'router' log = logging.getLogger(__name__ + '.CreateRouter') + _formatters = {'external_gateway_info': _format_external_gateway_info, } def add_known_arguments(self, parser): parser.add_argument( From 53c097aea689b777ed939ab7fa175c220b471796 Mon Sep 17 00:00:00 2001 From: Akihiro MOTOKI Date: Mon, 3 Dec 2012 13:42:00 +0900 Subject: [PATCH 09/29] Display columns in the order of -c options Fixes bug 1085785 Change-Id: Ifd8ddfbb7fa5e32be96febe981a6baf57b5e58c7 --- quantumclient/quantum/v2_0/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quantumclient/quantum/v2_0/__init__.py b/quantumclient/quantum/v2_0/__init__.py index b067d32d4..c73795b1c 100644 --- a/quantumclient/quantum/v2_0/__init__.py +++ b/quantumclient/quantum/v2_0/__init__.py @@ -387,7 +387,9 @@ def get_data(self, parsed_args): if not _columns: # clean the parsed_args.columns so that cliff will not break parsed_args.columns = [] - elif not parsed_args.columns and self.list_columns: + elif parsed_args.columns: + _columns = [x for x in parsed_args.columns if x in _columns] + elif self.list_columns: # if no -c(s) by user and list_columns, we use columns in # both list_columns and returned resource. # Also Keep their order the same as in list_columns From 964ae725d505c263ad6c069a81b80aef21132f71 Mon Sep 17 00:00:00 2001 From: Akihiro MOTOKI Date: Mon, 3 Dec 2012 20:10:38 +0900 Subject: [PATCH 10/29] Add --router and --floatingip to quota-update options. Fixes bug 1085820 Change-Id: I333b21414ec159cbf8474d47bd8ba3c6bd3cf8ad --- quantumclient/quantum/v2_0/quota.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/quantumclient/quantum/v2_0/quota.py b/quantumclient/quantum/v2_0/quota.py index 2a6979c33..0e749491e 100644 --- a/quantumclient/quantum/v2_0/quota.py +++ b/quantumclient/quantum/v2_0/quota.py @@ -164,6 +164,12 @@ def get_parser(self, prog_name): parser.add_argument( '--port', metavar='ports', help='the limit of port quota') + parser.add_argument( + '--router', metavar='routers', + help='the limit of router quota') + parser.add_argument( + '--floatingip', metavar='floatingips', + help='the limit of floating IP quota') quantumv20.add_extra_argument( parser, 'value_specs', 'new values for the %s' % self.resource) @@ -183,7 +189,7 @@ def get_data(self, parsed_args): quantum_client = self.get_client() quantum_client.format = parsed_args.request_format quota = {} - for resource in ('network', 'subnet', 'port'): + for resource in ('network', 'subnet', 'port', 'router', 'floatingip'): if getattr(parsed_args, resource): quota[resource] = self._validate_int( resource, From 3d423a35d0d97bdd8bcabd352eded5cda51a5a67 Mon Sep 17 00:00:00 2001 From: Gary Kotton Date: Wed, 5 Dec 2012 12:17:00 +0000 Subject: [PATCH 11/29] Ensures that help alignment is not hard coded Fixes bug 1086770 Change-Id: I6fa3edea83783e274d78e0c0195bca69d63b6e04 --- quantumclient/shell.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/quantumclient/shell.py b/quantumclient/shell.py index 34f1a9992..0beae19dc 100644 --- a/quantumclient/shell.py +++ b/quantumclient/shell.py @@ -141,6 +141,8 @@ class HelpAction(argparse.Action): 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.stdout.write('\nCommands for API v%s:\n' % app.api_version) @@ -149,7 +151,10 @@ def __call__(self, parser, namespace, values, option_string=None): factory = ep.load() cmd = factory(self, None) one_liner = cmd.get_description().split('\n')[0] - app.stdout.write(' %-25s %s\n' % (name, one_liner)) + 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) From 97b5e16df16b5a74fc43b5897940360c81e14222 Mon Sep 17 00:00:00 2001 From: Akihiro MOTOKI Date: Sat, 8 Dec 2012 03:28:45 +0900 Subject: [PATCH 12/29] Fix a wrong substition for '-h' in bash completion Fixes bug #1087806 Change-Id: Id39cac821922ca6adea296335f2395fabb011754 --- tools/quantum.bash_completion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/quantum.bash_completion b/tools/quantum.bash_completion index 45db025af..311f53309 100644 --- a/tools/quantum.bash_completion +++ b/tools/quantum.bash_completion @@ -9,7 +9,7 @@ _quantum() prev="${COMP_WORDS[COMP_CWORD-1]}" if [ "x$_quantum_opts" == "x" ] ; then - nbc="`quantum bash-completion | sed -e "s/\s-h\s/\s/"`" + nbc="`quantum bash-completion`" _quantum_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" _quantum_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" _quantum_opts_exp="`echo "$_quantum_opts" | sed -e "s/\s/|/g"`" From 53a6bece3040cdfb5ad85768debb6e5086af3c59 Mon Sep 17 00:00:00 2001 From: Akihiro MOTOKI Date: Mon, 3 Dec 2012 20:41:55 +0900 Subject: [PATCH 13/29] Add --dns-nameserver, --host-route, --disable-dhcp to subnet-create Fixes bug 1042982 Change-Id: I71f7e54a0982ac5fd188986d902667bd3fd6b219 --- quantumclient/quantum/v2_0/subnet.py | 38 +++++--- quantumclient/tests/unit/test_cli20_subnet.py | 88 +++++++++++++++++++ 2 files changed, 114 insertions(+), 12 deletions(-) diff --git a/quantumclient/quantum/v2_0/subnet.py b/quantumclient/quantum/v2_0/subnet.py index 0e67cb948..390f7bf82 100644 --- a/quantumclient/quantum/v2_0/subnet.py +++ b/quantumclient/quantum/v2_0/subnet.py @@ -98,15 +98,27 @@ def add_known_arguments(self, parser): default=False, action='store_true', help='No distribution of gateway') parser.add_argument( - '--allocation-pool', - action='append', - help='Allocation pool IP addresses for this subnet: ' - 'start=,end= ' - 'can be repeated') + '--allocation-pool', metavar='start=IP_ADDR,end=IP_ADDR', + action='append', dest='allocation_pools', type=utils.str2dict, + help='Allocation pool IP addresses for this subnet ' + '(This option can be repeated)') parser.add_argument( '--allocation_pool', - action='append', + action='append', dest='allocation_pools', type=utils.str2dict, help=argparse.SUPPRESS) + parser.add_argument( + '--host-route', metavar='destination=CIDR,nexthop=IP_ADDR', + action='append', dest='host_routes', type=utils.str2dict, + help='Additional route (This option can be repeated)') + parser.add_argument( + '--dns-nameserver', metavar='DNS_NAMESERVER', + action='append', dest='dns_nameservers', + help='DNS name server for this subnet ' + '(This option can be repeated)') + parser.add_argument( + '--disable-dhcp', + action='store_true', + help='Disable DHCP for this subnet') parser.add_argument( 'network_id', metavar='network', help='Network id or name this subnet belongs to') @@ -133,12 +145,14 @@ def args2body(self, parsed_args): body['subnet'].update({'tenant_id': parsed_args.tenant_id}) if parsed_args.name: body['subnet'].update({'name': parsed_args.name}) - ips = [] - if parsed_args.allocation_pool: - for ip_spec in parsed_args.allocation_pool: - ips.append(utils.str2dict(ip_spec)) - if ips: - body['subnet'].update({'allocation_pools': ips}) + if parsed_args.disable_dhcp: + body['subnet'].update({'enable_dhcp': False}) + if parsed_args.allocation_pools: + body['subnet']['allocation_pools'] = parsed_args.allocation_pools + if parsed_args.host_routes: + body['subnet']['host_routes'] = parsed_args.host_routes + if parsed_args.dns_nameservers: + body['subnet']['dns_nameservers'] = parsed_args.dns_nameservers return body diff --git a/quantumclient/tests/unit/test_cli20_subnet.py b/quantumclient/tests/unit/test_cli20_subnet.py index 77bf0674e..6bcabe865 100644 --- a/quantumclient/tests/unit/test_cli20_subnet.py +++ b/quantumclient/tests/unit/test_cli20_subnet.py @@ -151,6 +151,94 @@ def test_create_subnet_allocation_pools(self): position_names, position_values, tenant_id='tenantid') + def test_create_subnet_host_route(self): + """Create subnet: --tenant_id tenantid netid cidr. + The is + --host-route destination=172.16.1.0/24,nexthop=1.1.1.20 + """ + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--host-route', 'destination=172.16.1.0/24,nexthop=1.1.1.20', + netid, cidr] + position_names = ['ip_version', 'host_routes', 'network_id', + 'cidr'] + route = [{'destination': '172.16.1.0/24', 'nexthop': '1.1.1.20'}] + position_values = [4, route, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_host_routes(self): + """Create subnet: --tenant-id tenantid netid cidr. + The are + --host-route destination=172.16.1.0/24,nexthop=1.1.1.20 and + --host-route destination=172.17.7.0/24,nexthop=1.1.1.40 + """ + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--host-route', 'destination=172.16.1.0/24,nexthop=1.1.1.20', + '--host-route', 'destination=172.17.7.0/24,nexthop=1.1.1.40', + netid, cidr] + position_names = ['ip_version', 'host_routes', 'network_id', + 'cidr'] + routes = [{'destination': '172.16.1.0/24', 'nexthop': '1.1.1.20'}, + {'destination': '172.17.7.0/24', 'nexthop': '1.1.1.40'}] + position_values = [4, routes, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_dns_nameservers(self): + """Create subnet: --tenant-id tenantid netid cidr. + The are + --dns-nameserver 1.1.1.20 and --dns-nameserver 1.1.1.40 + """ + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--dns-nameserver', '1.1.1.20', + '--dns-nameserver', '1.1.1.40', + netid, cidr] + position_names = ['ip_version', 'dns_nameservers', 'network_id', + 'cidr'] + nameservers = ['1.1.1.20', '1.1.1.40'] + position_values = [4, nameservers, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_with_disable_dhcp(self): + """Create subnet: --tenant-id tenantid --disable-dhcp netid cidr.""" + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--disable-dhcp', + netid, cidr] + position_names = ['ip_version', 'enable_dhcp', 'network_id', + 'cidr'] + position_values = [4, False, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + def test_list_subnets_detail(self): """List subnets: -D.""" resources = "subnets" From f328f68ab0083741b1424fd4002670d1103cfaff Mon Sep 17 00:00:00 2001 From: Ben Andrews Date: Sun, 16 Dec 2012 20:39:18 -0500 Subject: [PATCH 14/29] bug 1091028 sets version that pip can use for pyparser to one that is for python 2.X. 2.0.0 is only for python 3 Change-Id: I8e23e576032290a71ba929af5f5c729ae3dffca6 Fixes: bug #1091028 --- tools/pip-requires | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pip-requires b/tools/pip-requires index 6183e4e1b..2deb910ce 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -3,4 +3,4 @@ argparse httplib2 prettytable>=0.6.0 simplejson -pyparsing +pyparsing>=1.5.6,<2.0 From 29a2dbb696c65735b200a5bb8b777af4833e99cd Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Wed, 19 Dec 2012 15:10:17 +0100 Subject: [PATCH 15/29] Add file 'ChangeLog' to MANIFEST.in The file is missing from tarballs released at tarballs.openstack.org. Change-Id: Id782305495033948b05ee66da3d0ee2ff1933c20 --- .gitignore | 1 + MANIFEST.in | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 15eae7e22..1a5761972 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ run_tests.log doc/build/ doc/source/api/ quantumclient/versioninfo +ChangeLog diff --git a/MANIFEST.in b/MANIFEST.in index 62ce0fa62..41d72419d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include tox.ini include LICENSE README HACKING.rst +include ChangeLog include tools/* include quantumclient/tests/* include quantumclient/tests/unit/* From 3b49d2299361405e5893c8eec4b26b3c0d5b8d55 Mon Sep 17 00:00:00 2001 From: Zhongyue Luo Date: Mon, 31 Dec 2012 22:25:00 +0800 Subject: [PATCH 16/29] Fix import order nits Change-Id: I7c95fa2db1d719f6ed34468ad12b7a9e4c9e794d --- quantumclient/common/clientmanager.py | 5 +++-- quantumclient/quantum/v2_0/network.py | 2 +- quantumclient/shell.py | 1 + quantumclient/tests/unit/test_auth.py | 10 +++++----- quantumclient/tests/unit/test_cli20.py | 2 +- quantumclient/tests/unit/test_cli20_network.py | 13 +++++++------ quantumclient/tests/unit/test_cli20_port.py | 13 +++++++------ quantumclient/tests/unit/test_cli20_subnet.py | 8 ++++---- quantumclient/v2_0/client.py | 15 +-------------- 9 files changed, 30 insertions(+), 39 deletions(-) diff --git a/quantumclient/common/clientmanager.py b/quantumclient/common/clientmanager.py index 1c1f1acd0..7346aa541 100644 --- a/quantumclient/common/clientmanager.py +++ b/quantumclient/common/clientmanager.py @@ -20,10 +20,11 @@ import logging +from quantumclient.client import HTTPClient from quantumclient.common import exceptions as exc - from quantumclient.quantum import client as quantum_client -from quantumclient.client import HTTPClient + + LOG = logging.getLogger(__name__) diff --git a/quantumclient/quantum/v2_0/network.py b/quantumclient/quantum/v2_0/network.py index 070de54c4..19b9978f2 100644 --- a/quantumclient/quantum/v2_0/network.py +++ b/quantumclient/quantum/v2_0/network.py @@ -21,8 +21,8 @@ from quantumclient.quantum.v2_0 import CreateCommand from quantumclient.quantum.v2_0 import DeleteCommand from quantumclient.quantum.v2_0 import ListCommand -from quantumclient.quantum.v2_0 import UpdateCommand from quantumclient.quantum.v2_0 import ShowCommand +from quantumclient.quantum.v2_0 import UpdateCommand def _format_subnets(network): diff --git a/quantumclient/shell.py b/quantumclient/shell.py index 0beae19dc..36f3e0cc2 100644 --- a/quantumclient/shell.py +++ b/quantumclient/shell.py @@ -18,6 +18,7 @@ """ Command-line interface to the Quantum APIs """ + import argparse import gettext import logging diff --git a/quantumclient/tests/unit/test_auth.py b/quantumclient/tests/unit/test_auth.py index 9f270b5b6..477ba904f 100644 --- a/quantumclient/tests/unit/test_auth.py +++ b/quantumclient/tests/unit/test_auth.py @@ -15,16 +15,16 @@ # # vim: tabstop=4 shiftwidth=4 softtabstop=4 -import mox -from mox import ContainsKeyValue, IsA, StrContains -import unittest - import httplib2 import json +import unittest import uuid -from quantumclient.common import exceptions +import mox +from mox import ContainsKeyValue, IsA, StrContains + from quantumclient.client import HTTPClient +from quantumclient.common import exceptions USERNAME = 'testuser' diff --git a/quantumclient/tests/unit/test_cli20.py b/quantumclient/tests/unit/test_cli20.py index 15ca12270..950c23d17 100644 --- a/quantumclient/tests/unit/test_cli20.py +++ b/quantumclient/tests/unit/test_cli20.py @@ -19,8 +19,8 @@ import unittest import mox -from mox import ContainsKeyValue from mox import Comparator +from mox import ContainsKeyValue from quantumclient.quantum import v2_0 as quantumv20 from quantumclient.v2_0.client import Client diff --git a/quantumclient/tests/unit/test_cli20_network.py b/quantumclient/tests/unit/test_cli20_network.py index 5360c08cc..1ef02150a 100644 --- a/quantumclient/tests/unit/test_cli20_network.py +++ b/quantumclient/tests/unit/test_cli20_network.py @@ -16,19 +16,20 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 import sys + from mox import ContainsKeyValue from quantumclient.common import exceptions from quantumclient.common import utils -from quantumclient.tests.unit import test_cli20 -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp from quantumclient.quantum.v2_0.network import CreateNetwork -from quantumclient.quantum.v2_0.network import ListNetwork -from quantumclient.quantum.v2_0.network import UpdateNetwork -from quantumclient.quantum.v2_0.network import ShowNetwork from quantumclient.quantum.v2_0.network import DeleteNetwork from quantumclient.quantum.v2_0.network import ListExternalNetwork +from quantumclient.quantum.v2_0.network import ListNetwork +from quantumclient.quantum.v2_0.network import ShowNetwork +from quantumclient.quantum.v2_0.network import UpdateNetwork +from quantumclient.tests.unit import test_cli20 +from quantumclient.tests.unit.test_cli20 import CLITestV20Base +from quantumclient.tests.unit.test_cli20 import MyApp class CLITestV20Network(CLITestV20Base): diff --git a/quantumclient/tests/unit/test_cli20_port.py b/quantumclient/tests/unit/test_cli20_port.py index 6ddbf319c..bd776d4a0 100644 --- a/quantumclient/tests/unit/test_cli20_port.py +++ b/quantumclient/tests/unit/test_cli20_port.py @@ -16,17 +16,18 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 import sys + from mox import ContainsKeyValue -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp -from quantumclient.tests.unit import test_cli20 from quantumclient.quantum.v2_0.port import CreatePort -from quantumclient.quantum.v2_0.port import ListPort -from quantumclient.quantum.v2_0.port import UpdatePort -from quantumclient.quantum.v2_0.port import ShowPort from quantumclient.quantum.v2_0.port import DeletePort +from quantumclient.quantum.v2_0.port import ListPort from quantumclient.quantum.v2_0.port import ListRouterPort +from quantumclient.quantum.v2_0.port import ShowPort +from quantumclient.quantum.v2_0.port import UpdatePort +from quantumclient.tests.unit import test_cli20 +from quantumclient.tests.unit.test_cli20 import CLITestV20Base +from quantumclient.tests.unit.test_cli20 import MyApp class CLITestV20Port(CLITestV20Base): diff --git a/quantumclient/tests/unit/test_cli20_subnet.py b/quantumclient/tests/unit/test_cli20_subnet.py index 6bcabe865..e07f6c712 100644 --- a/quantumclient/tests/unit/test_cli20_subnet.py +++ b/quantumclient/tests/unit/test_cli20_subnet.py @@ -17,13 +17,13 @@ import sys -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp from quantumclient.quantum.v2_0.subnet import CreateSubnet +from quantumclient.quantum.v2_0.subnet import DeleteSubnet from quantumclient.quantum.v2_0.subnet import ListSubnet -from quantumclient.quantum.v2_0.subnet import UpdateSubnet from quantumclient.quantum.v2_0.subnet import ShowSubnet -from quantumclient.quantum.v2_0.subnet import DeleteSubnet +from quantumclient.quantum.v2_0.subnet import UpdateSubnet +from quantumclient.tests.unit.test_cli20 import CLITestV20Base +from quantumclient.tests.unit.test_cli20 import MyApp class CLITestV20Subnet(CLITestV20Base): diff --git a/quantumclient/v2_0/client.py b/quantumclient/v2_0/client.py index 6ec752def..f30f3fdef 100644 --- a/quantumclient/v2_0/client.py +++ b/quantumclient/v2_0/client.py @@ -24,6 +24,7 @@ from quantumclient.common import exceptions from quantumclient.common.serializer import Serializer + _logger = logging.getLogger(__name__) @@ -532,17 +533,3 @@ def post(self, action, body=None, headers=None, params=None): def put(self, action, body=None, headers=None, params=None): return self.retry_request("PUT", action, body=body, headers=headers, params=params) - -#if __name__ == '__main__': -# -# client20 = Client(username='admin', -# password='password', -# auth_url='http://localhost:5000/v2.0', -# tenant_name='admin') -# client20 = Client(token='ec796583fcad4aa690b723bc0b25270e', -# endpoint_url='http://localhost:9696') -# -# client20.tenant = 'default' -# client20.format = 'json' -# nets = client20.list_networks() -# print nets From 91734d2b87064879c3aab76de4f732e7f8d8f2b2 Mon Sep 17 00:00:00 2001 From: gongysh Date: Tue, 11 Dec 2012 23:40:16 +0800 Subject: [PATCH 17/29] Allow known options defined after position arguments. We run the argument parser to split known options and unknown options. Make '-' work and have the same effect as '_' in both known and unknown option parts. Make metavar Uppercase. blueprint options-location Change-Id: Ic27b278484133c8b83e3b031a0810a76b050219f --- quantumclient/quantum/v2_0/__init__.py | 70 ++++++++++++++----- quantumclient/quantum/v2_0/floatingip.py | 4 +- quantumclient/quantum/v2_0/network.py | 2 +- quantumclient/quantum/v2_0/port.py | 2 +- quantumclient/quantum/v2_0/router.py | 2 +- quantumclient/quantum/v2_0/subnet.py | 8 +-- quantumclient/shell.py | 40 +++++++++++ quantumclient/tests/unit/test_casual_args.py | 2 +- quantumclient/tests/unit/test_cli20.py | 5 +- .../tests/unit/test_cli20_floatingips.py | 4 +- quantumclient/tests/unit/test_cli20_subnet.py | 61 ++++++++++++++++ 11 files changed, 168 insertions(+), 32 deletions(-) diff --git a/quantumclient/quantum/v2_0/__init__.py b/quantumclient/quantum/v2_0/__init__.py index c73795b1c..5eae661a9 100644 --- a/quantumclient/quantum/v2_0/__init__.py +++ b/quantumclient/quantum/v2_0/__init__.py @@ -77,11 +77,17 @@ def add_show_list_common_argument(parser): action='store_true', help=argparse.SUPPRESS) parser.add_argument( - '-F', '--fields', + '--fields', + help=argparse.SUPPRESS, + action='append', + default=[]) + parser.add_argument( + '-F', '--field', + dest='fields', metavar='FIELD', help='specify the field(s) to be returned by server,' ' can be repeated', action='append', - default=[], ) + default=[]) def add_extra_argument(parser, name, _help): @@ -108,15 +114,16 @@ def parse_args_to_dict(values_specs): ''' # -- is a pseudo argument - if values_specs and values_specs[0] == '--': - del values_specs[0] + values_specs_copy = values_specs[:] + if values_specs_copy and values_specs_copy[0] == '--': + del values_specs_copy[0] _options = {} current_arg = None _values_specs = [] _value_number = 0 _list_flag = False current_item = None - for _item in values_specs: + for _item in values_specs_copy: if _item.startswith('--'): if current_arg is not None: if _value_number > 1 or _list_flag: @@ -166,22 +173,49 @@ def parse_args_to_dict(values_specs): current_arg.update({'nargs': '+'}) elif _value_number == 0: current_arg.update({'action': 'store_true'}) - _parser = argparse.ArgumentParser(add_help=False) - for opt, optspec in _options.iteritems(): - _parser.add_argument(opt, **optspec) - _args = _parser.parse_args(_values_specs) + _args = None + if _values_specs: + _parser = argparse.ArgumentParser(add_help=False) + for opt, optspec in _options.iteritems(): + _parser.add_argument(opt, **optspec) + _args = _parser.parse_args(_values_specs) result_dict = {} - for opt in _options.iterkeys(): - _opt = opt.split('--', 2)[1] - _value = getattr(_args, _opt.replace('-', '_')) - if _value is not None: - result_dict.update({_opt: _value}) + if _args: + for opt in _options.iterkeys(): + _opt = opt.split('--', 2)[1] + _opt = _opt.replace('-', '_') + _value = getattr(_args, _opt) + if _value is not None: + result_dict.update({_opt: _value}) return result_dict +def _merge_args(qCmd, parsed_args, _extra_values, value_specs): + """Merge arguments from _extra_values into parsed_args. + + If an argument value are provided in both and it is a list, + the values in _extra_values will be merged into parsed_args. + + @param parsed_args: the parsed args from known options + @param _extra_values: the other parsed arguments in unknown parts + @param values_specs: the unparsed unknown parts + """ + temp_values = _extra_values.copy() + for key, value in temp_values.iteritems(): + if hasattr(parsed_args, key): + arg_value = getattr(parsed_args, key) + if arg_value is not None and value is not None: + if isinstance(arg_value, list): + if value and isinstance(value, list): + if type(arg_value[0]) == type(value[0]): + arg_value.extend(value) + _extra_values.pop(key) + + class QuantumCommand(command.OpenStackCommand): api = 'network' log = logging.getLogger(__name__ + '.QuantumCommand') + values_specs = [] def get_client(self): return self.app.client_manager.quantum @@ -227,14 +261,12 @@ class CreateCommand(QuantumCommand, show.ShowOne): def get_parser(self, prog_name): parser = super(CreateCommand, self).get_parser(prog_name) parser.add_argument( - '--tenant-id', metavar='tenant-id', + '--tenant-id', metavar='TENANT_ID', help=_('the owner tenant ID'), ) parser.add_argument( '--tenant_id', help=argparse.SUPPRESS) self.add_known_arguments(parser) - add_extra_argument(parser, 'value_specs', - 'new values for the %s' % self.resource) return parser def add_known_arguments(self, parser): @@ -247,8 +279,10 @@ def get_data(self, parsed_args): self.log.debug('get_data(%s)' % parsed_args) quantum_client = self.get_client() quantum_client.format = parsed_args.request_format + _extra_values = parse_args_to_dict(self.values_specs) + _merge_args(self, parsed_args, _extra_values, + self.values_specs) body = self.args2body(parsed_args) - _extra_values = parse_args_to_dict(parsed_args.value_specs) body[self.resource].update(_extra_values) obj_creator = getattr(quantum_client, "create_%s" % self.resource) diff --git a/quantumclient/quantum/v2_0/floatingip.py b/quantumclient/quantum/v2_0/floatingip.py index 5613af866..3198dfeb7 100644 --- a/quantumclient/quantum/v2_0/floatingip.py +++ b/quantumclient/quantum/v2_0/floatingip.py @@ -52,8 +52,8 @@ class CreateFloatingIP(CreateCommand): def add_known_arguments(self, parser): parser.add_argument( - 'floating_network_id', - help='Network to allocate floating IP from') + 'floating_network_id', metavar='FLOATING_NETWORK', + help='Network name or id to allocate floating IP from') parser.add_argument( '--port-id', help='ID of the port to be associated with the floatingip') diff --git a/quantumclient/quantum/v2_0/network.py b/quantumclient/quantum/v2_0/network.py index 19b9978f2..4d88ab3d3 100644 --- a/quantumclient/quantum/v2_0/network.py +++ b/quantumclient/quantum/v2_0/network.py @@ -84,7 +84,7 @@ def add_known_arguments(self, parser): default=argparse.SUPPRESS, help='Set the network as shared') parser.add_argument( - 'name', metavar='name', + 'name', metavar='NAME', help='Name of network to create') def args2body(self, parsed_args): diff --git a/quantumclient/quantum/v2_0/port.py b/quantumclient/quantum/v2_0/port.py index 7f393654a..98661b57d 100644 --- a/quantumclient/quantum/v2_0/port.py +++ b/quantumclient/quantum/v2_0/port.py @@ -112,7 +112,7 @@ def add_known_arguments(self, parser): action='append', help='desired IP for this port: ' 'subnet_id=,ip_address=, ' - 'can be repeated') + '(This option can be repeated.)') parser.add_argument( '--fixed_ip', action='append', diff --git a/quantumclient/quantum/v2_0/router.py b/quantumclient/quantum/v2_0/router.py index db85b8057..a0de0c835 100644 --- a/quantumclient/quantum/v2_0/router.py +++ b/quantumclient/quantum/v2_0/router.py @@ -68,7 +68,7 @@ def add_known_arguments(self, parser): action='store_false', help=argparse.SUPPRESS) parser.add_argument( - 'name', metavar='name', + 'name', metavar='NAME', help='Name of router to create') def args2body(self, parsed_args): diff --git a/quantumclient/quantum/v2_0/subnet.py b/quantumclient/quantum/v2_0/subnet.py index 390f7bf82..9339ff58b 100644 --- a/quantumclient/quantum/v2_0/subnet.py +++ b/quantumclient/quantum/v2_0/subnet.py @@ -91,7 +91,7 @@ def add_known_arguments(self, parser): choices=[4, 6], help=argparse.SUPPRESS) parser.add_argument( - '--gateway', metavar='gateway', + '--gateway', metavar='GATEWAY_IP', help='gateway ip of this subnet') parser.add_argument( '--no-gateway', @@ -120,10 +120,10 @@ def add_known_arguments(self, parser): action='store_true', help='Disable DHCP for this subnet') parser.add_argument( - 'network_id', metavar='network', - help='Network id or name this subnet belongs to') + 'network_id', metavar='NETWORK', + help='network id or name this subnet belongs to') parser.add_argument( - 'cidr', metavar='cidr', + 'cidr', metavar='CIDR', help='cidr of subnet to create') def args2body(self, parsed_args): diff --git a/quantumclient/shell.py b/quantumclient/shell.py index 36f3e0cc2..a95bf0985 100644 --- a/quantumclient/shell.py +++ b/quantumclient/shell.py @@ -349,6 +349,46 @@ def run(self, argv): 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) + known_args, values_specs = cmd_parser.parse_known_args(sub_argv) + cmd.values_specs = values_specs + result = cmd.run(known_args) + except Exception as err: + if self.options.debug: + self.log.exception(err) + else: + self.log.error(err) + try: + self.clean_up(cmd, result, err) + except Exception as err2: + if self.options.debug: + self.log.exception(err2) + else: + self.log.error('Could not clean up: %s', err2) + if self.options.debug: + raise + else: + try: + self.clean_up(cmd, result, None) + except Exception as err3: + if self.options.debug: + self.log.exception(err3) + else: + self.log.error('Could not clean up: %s', err3) + return result + def authenticate_user(self): """Make sure the user has provided all of the authentication info we need. diff --git a/quantumclient/tests/unit/test_casual_args.py b/quantumclient/tests/unit/test_casual_args.py index 4192ff81b..4eb2b6622 100644 --- a/quantumclient/tests/unit/test_casual_args.py +++ b/quantumclient/tests/unit/test_casual_args.py @@ -35,7 +35,7 @@ def test_default_bool(self): def test_bool_true(self): _specs = ['--my-bool', 'type=bool', 'true', '--arg1', 'value1'] _mydict = quantumV20.parse_args_to_dict(_specs) - self.assertTrue(_mydict['my-bool']) + self.assertTrue(_mydict['my_bool']) def test_bool_false(self): _specs = ['--my_bool', 'type=bool', 'false', '--arg1', 'value1'] diff --git a/quantumclient/tests/unit/test_cli20.py b/quantumclient/tests/unit/test_cli20.py index 950c23d17..4c8f61cd1 100644 --- a/quantumclient/tests/unit/test_cli20.py +++ b/quantumclient/tests/unit/test_cli20.py @@ -168,8 +168,9 @@ def _test_create_resource(self, resource, cmd, resstr)) self.mox.ReplayAll() cmd_parser = cmd.get_parser('create_' + resource) - parsed_args = cmd_parser.parse_args(args) - cmd.run(parsed_args) + known_args, values_specs = cmd_parser.parse_known_args(args) + cmd.values_specs = values_specs + cmd.run(known_args) self.mox.VerifyAll() self.mox.UnsetStubs() _str = self.fake_stdout.make_string() diff --git a/quantumclient/tests/unit/test_cli20_floatingips.py b/quantumclient/tests/unit/test_cli20_floatingips.py index e48d29e86..7d0da0424 100644 --- a/quantumclient/tests/unit/test_cli20_floatingips.py +++ b/quantumclient/tests/unit/test_cli20_floatingips.py @@ -57,7 +57,7 @@ def test_create_floatingip_and_port(self): # Test dashed options args = [name, '--port-id', pid] - position_names = ['floating_network_id', 'port-id'] + position_names = ['floating_network_id', 'port_id'] _str = self._test_create_resource(resource, cmd, name, myid, args, position_names, position_values) @@ -76,7 +76,7 @@ def test_create_floatingip_and_port_and_address(self): position_names, position_values) # Test dashed options args = [name, '--port-id', pid, '--fixed-ip-address', addr] - position_names = ['floating_network_id', 'port-id', 'fixed-ip-address'] + position_names = ['floating_network_id', 'port_id', 'fixed_ip_address'] _str = self._test_create_resource(resource, cmd, name, myid, args, position_names, position_values) diff --git a/quantumclient/tests/unit/test_cli20_subnet.py b/quantumclient/tests/unit/test_cli20_subnet.py index e07f6c712..ec4396fd5 100644 --- a/quantumclient/tests/unit/test_cli20_subnet.py +++ b/quantumclient/tests/unit/test_cli20_subnet.py @@ -239,6 +239,67 @@ def test_create_subnet_with_disable_dhcp(self): position_names, position_values, tenant_id='tenantid') + def test_create_subnet_merge_single_plurar(self): + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--allocation-pool', 'start=1.1.1.10,end=1.1.1.20', + netid, cidr, + '--allocation-pools', 'list=true', 'type=dict', + 'start=1.1.1.30,end=1.1.1.40'] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, + {'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_merge_plurar(self): + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + netid, cidr, + '--allocation-pools', 'list=true', 'type=dict', + 'start=1.1.1.30,end=1.1.1.40'] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_merge_single_single(self): + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--allocation-pool', 'start=1.1.1.10,end=1.1.1.20', + netid, cidr, + '--allocation-pool', + 'start=1.1.1.30,end=1.1.1.40'] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, + {'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + def test_list_subnets_detail(self): """List subnets: -D.""" resources = "subnets" From f288e2a2f9ce9bfd175477086c225f04a1cf6cc2 Mon Sep 17 00:00:00 2001 From: Akihiro MOTOKI Date: Thu, 10 Jan 2013 03:22:14 +0900 Subject: [PATCH 18/29] Display subnet cidr information in net-list Fixes bug 1074415 This commit introduces extend_list() in ListCommand class. This method can be used to update a retrieved list (e.g., add additional information or convert some field to more human-readable value). Change-Id: Icf5ab616ab4d9add16c921e1944ba37b376b2ab2 --- quantumclient/quantum/v2_0/__init__.py | 30 ++++-- quantumclient/quantum/v2_0/network.py | 24 +++-- .../tests/unit/test_cli20_network.py | 101 +++++++++++++++--- 3 files changed, 127 insertions(+), 28 deletions(-) diff --git a/quantumclient/quantum/v2_0/__init__.py b/quantumclient/quantum/v2_0/__init__.py index 5eae661a9..530b07a84 100644 --- a/quantumclient/quantum/v2_0/__init__.py +++ b/quantumclient/quantum/v2_0/__init__.py @@ -375,7 +375,7 @@ def run(self, parsed_args): class ListCommand(QuantumCommand, lister.Lister): - """List resourcs that belong to a given tenant + """List resources that belong to a given tenant """ @@ -391,11 +391,10 @@ def get_parser(self, prog_name): add_extra_argument(parser, 'filter_specs', 'filters options') return parser - def get_data(self, parsed_args): - self.log.debug('get_data(%s)' % parsed_args) + def retrieve_list(self, parsed_args): + """Retrieve a list of resources from Quantum server""" quantum_client = self.get_client() search_opts = parse_args_to_dict(parsed_args.filter_specs) - self.log.debug('search options: %s', search_opts) quantum_client.format = parsed_args.request_format fields = parsed_args.fields @@ -411,12 +410,21 @@ def get_data(self, parsed_args): search_opts.update({'verbose': 'True'}) obj_lister = getattr(quantum_client, "list_%ss" % self.resource) - data = obj_lister(**search_opts) - info = [] + collection = self.resource + "s" - if collection in data: - info = data[collection] + return data.get(collection, []) + + def extend_list(self, data, parsed_args): + """Update a retrieved list. + + This method provides a way to modify a original list returned from + the quantum server. For example, you can add subnet cidr information + to a list network. + """ + pass + + def setup_columns(self, info, parsed_args): _columns = len(info) > 0 and sorted(info[0].keys()) or [] if not _columns: # clean the parsed_args.columns so that cliff will not break @@ -432,6 +440,12 @@ def get_data(self, parsed_args): s, _columns, formatters=self._formatters, ) for s in info), ) + def get_data(self, parsed_args): + self.log.debug('get_data(%s)' % parsed_args) + data = self.retrieve_list(parsed_args) + self.extend_list(data, parsed_args) + return self.setup_columns(data, parsed_args) + class ShowCommand(QuantumCommand, show.ShowOne): """Show information of a given resource diff --git a/quantumclient/quantum/v2_0/network.py b/quantumclient/quantum/v2_0/network.py index 4d88ab3d3..b714ae59e 100644 --- a/quantumclient/quantum/v2_0/network.py +++ b/quantumclient/quantum/v2_0/network.py @@ -18,6 +18,7 @@ import argparse import logging +from quantumclient.common import utils from quantumclient.quantum.v2_0 import CreateCommand from quantumclient.quantum.v2_0 import DeleteCommand from quantumclient.quantum.v2_0 import ListCommand @@ -27,7 +28,8 @@ def _format_subnets(network): try: - return '\n'.join(network['subnets']) + return '\n'.join([' '.join([s['id'], s.get('cidr', '')]) + for s in network['subnets']]) except Exception: return '' @@ -40,20 +42,28 @@ class ListNetwork(ListCommand): _formatters = {'subnets': _format_subnets, } list_columns = ['id', 'name', 'subnets'] + def extend_list(self, data, parsed_args): + """Add subnet information to a network list""" + quantum_client = self.get_client() + search_opts = {'fields': ['id', 'cidr']} + subnets = quantum_client.list_subnets(**search_opts).get('subnets', []) + subnet_dict = dict([(s['id'], s) for s in subnets]) + for n in data: + if 'subnets' in n: + n['subnets'] = [(subnet_dict.get(s) or {"id": s}) + for s in n['subnets']] -class ListExternalNetwork(ListCommand): + +class ListExternalNetwork(ListNetwork): """List external networks that belong to a given tenant""" - resource = 'network' log = logging.getLogger(__name__ + '.ListExternalNetwork') - _formatters = {'subnets': _format_subnets, } - list_colums = ['id', 'name', 'subnets'] - def get_data(self, parsed_args): + def retrieve_list(self, parsed_args): if '--' not in parsed_args.filter_specs: parsed_args.filter_specs.append('--') parsed_args.filter_specs.append('--router:external=True') - return super(ListExternalNetwork, self).get_data(parsed_args) + return super(ListExternalNetwork, self).retrieve_list(parsed_args) class ShowNetwork(ShowCommand): diff --git a/quantumclient/tests/unit/test_cli20_network.py b/quantumclient/tests/unit/test_cli20_network.py index 1ef02150a..8eebcfe6c 100644 --- a/quantumclient/tests/unit/test_cli20_network.py +++ b/quantumclient/tests/unit/test_cli20_network.py @@ -17,7 +17,8 @@ import sys -from mox import ContainsKeyValue +import mox +from mox import (ContainsKeyValue, IgnoreArg, IsA) from quantumclient.common import exceptions from quantumclient.common import utils @@ -101,6 +102,8 @@ def test_list_nets_empty_with_column(self): cmd = ListNetwork(MyApp(sys.stdout), None) self.mox.StubOutWithMock(cmd, "get_client") self.mox.StubOutWithMock(self.client.httpclient, "request") + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) cmd.get_client().MultipleTimes().AndReturn(self.client) reses = {resources: []} resstr = self.client.serialize(reses) @@ -125,40 +128,109 @@ def test_list_nets_empty_with_column(self): _str = self.fake_stdout.make_string() self.assertEquals('\n', _str) + def _test_list_networks(self, cmd, detail=False, tags=[], + fields_1=[], fields_2=[]): + resources = "networks" + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) + self._test_list_resources(resources, cmd, detail, tags, + fields_1, fields_2) + def test_list_nets_detail(self): """list nets: -D.""" - resources = "networks" cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) + self._test_list_networks(cmd, True) def test_list_nets_tags(self): """List nets: -- --tags a b.""" - resources = "networks" cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, tags=['a', 'b']) + self._test_list_networks(cmd, tags=['a', 'b']) def test_list_nets_detail_tags(self): """List nets: -D -- --tags a b.""" resources = "networks" cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, detail=True, tags=['a', 'b']) + self._test_list_networks(cmd, detail=True, tags=['a', 'b']) + + def _test_list_nets_extend_subnets(self, data, expected): + def setup_list_stub(resources, data, query): + reses = {resources: data} + resstr = self.client.serialize(reses) + resp = (test_cli20.MyResp(200), resstr) + path = getattr(self.client, resources + '_path') + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(resp) + + resources = "networks" + cmd = ListNetwork(test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(cmd, 'get_client') + self.mox.StubOutWithMock(self.client.httpclient, 'request') + cmd.get_client().AndReturn(self.client) + setup_list_stub('networks', data, '') + cmd.get_client().AndReturn(self.client) + setup_list_stub('subnets', + [{'id': 'mysubid1', 'cidr': '192.168.1.0/24'}, + {'id': 'mysubid2', 'cidr': '172.16.0.0/24'}, + {'id': 'mysubid3', 'cidr': '10.1.1.0/24'}], + query='fields=id&fields=cidr') + self.mox.ReplayAll() + + args = [] + cmd_parser = cmd.get_parser('list_networks') + parsed_args = cmd_parser.parse_args(args) + result = cmd.get_data(parsed_args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _result = [x for x in result[1]] + self.assertEqual(len(_result), len(expected)) + for res, exp in zip(_result, expected): + self.assertEqual(len(res), len(exp)) + for a, b in zip(res, exp): + self.assertEqual(a, b) + + def test_list_nets_extend_subnets(self): + data = [{'id': 'netid1', 'name': 'net1', 'subnets': ['mysubid1']}, + {'id': 'netid2', 'name': 'net2', 'subnets': ['mysubid2', + 'mysubid3']}] + # id, name, subnets + expected = [('netid1', 'net1', 'mysubid1 192.168.1.0/24'), + ('netid2', 'net2', + 'mysubid2 172.16.0.0/24\nmysubid3 10.1.1.0/24')] + self._test_list_nets_extend_subnets(data, expected) + + def test_list_nets_extend_subnets_no_subnet(self): + data = [{'id': 'netid1', 'name': 'net1', 'subnets': ['mysubid1']}, + {'id': 'netid2', 'name': 'net2', 'subnets': ['mysubid4']}] + # id, name, subnets + expected = [('netid1', 'net1', 'mysubid1 192.168.1.0/24'), + ('netid2', 'net2', 'mysubid4 ')] + self._test_list_nets_extend_subnets(data, expected) def test_list_nets_fields(self): """List nets: --fields a --fields b -- --fields c d.""" resources = "networks" cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, - fields_1=['a', 'b'], fields_2=['c', 'd']) + self._test_list_networks(cmd, + fields_1=['a', 'b'], fields_2=['c', 'd']) - def test_list_nets_defined_column(self): + def _test_list_nets_columns(self, cmd, returned_body, + args=['-f', 'json']): resources = 'networks' + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) + self._test_list_columns(cmd, resources, returned_body, args=args) + + def test_list_nets_defined_column(self): cmd = ListNetwork(MyApp(sys.stdout), None) returned_body = {"networks": [{"name": "buildname3", "id": "id3", "tenant_id": "tenant_3", "subnets": []}]} - self._test_list_columns(cmd, resources, returned_body, - args=['-f', 'json', '-c', 'id']) + self._test_list_nets_columns(cmd, returned_body, + args=['-f', 'json', '-c', 'id']) _str = self.fake_stdout.make_string() returned_networks = utils.loads(_str) self.assertEquals(1, len(returned_networks)) @@ -167,13 +239,12 @@ def test_list_nets_defined_column(self): self.assertEquals("id", network.keys()[0]) def test_list_nets_with_default_column(self): - resources = 'networks' cmd = ListNetwork(MyApp(sys.stdout), None) returned_body = {"networks": [{"name": "buildname3", "id": "id3", "tenant_id": "tenant_3", "subnets": []}]} - self._test_list_columns(cmd, resources, returned_body) + self._test_list_nets_columns(cmd, returned_body) _str = self.fake_stdout.make_string() returned_networks = utils.loads(_str) self.assertEquals(1, len(returned_networks)) @@ -187,6 +258,8 @@ def test_list_external_nets_empty_with_column(self): cmd = ListExternalNetwork(MyApp(sys.stdout), None) self.mox.StubOutWithMock(cmd, "get_client") self.mox.StubOutWithMock(self.client.httpclient, "request") + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) cmd.get_client().MultipleTimes().AndReturn(self.client) reses = {resources: []} resstr = self.client.serialize(reses) @@ -217,6 +290,8 @@ def _test_list_external_nets(self, resources, cmd, fields_1=[], fields_2=[]): self.mox.StubOutWithMock(cmd, "get_client") self.mox.StubOutWithMock(self.client.httpclient, "request") + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) cmd.get_client().MultipleTimes().AndReturn(self.client) reses = {resources: [{'id': 'myid1', }, {'id': 'myid2', }, ], } From 04deb2ac585937270f074fbe7ef13565d978e1ce Mon Sep 17 00:00:00 2001 From: Akihiro MOTOKI Date: Mon, 31 Dec 2012 02:47:53 +0900 Subject: [PATCH 19/29] Make "quantum help" to show a list of subcommands. Fix bug 1023260 All other OpenStack clients (nova, keystone, glance, ....) accepts just "help" which displays a list of subcommands. This commit makes quantum command consist with other OpenStack projects. After this change help behavior of quantum command becomes as follows: Show general help message: quantum --help quantum help Show help message of subcommand quantum help quantum --help Change-Id: I34ca0df809da04f9b9b9275c697e6aafca312cfe --- quantumclient/shell.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/quantumclient/shell.py b/quantumclient/shell.py index a95bf0985..36fb63da0 100644 --- a/quantumclient/shell.py +++ b/quantumclient/shell.py @@ -316,6 +316,7 @@ def run(self, argv): index = 0 command_pos = -1 help_pos = -1 + help_command_pos = -1 for arg in argv: if arg == 'bash-completion': self._bash_completion() @@ -326,9 +327,14 @@ def run(self, argv): 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 From c79c36a5cb8b92e04baf60fccf0eb6a9a73f55bd Mon Sep 17 00:00:00 2001 From: sthakkar Date: Mon, 21 Jan 2013 17:32:28 -0800 Subject: [PATCH 20/29] Fix quantum client example Example references an incorrect method. list_net() should be list_networks() Change-Id: Ia1f4e13898964da63cb9c9da75270236aa6587b6 --- quantumclient/v2_0/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantumclient/v2_0/client.py b/quantumclient/v2_0/client.py index f30f3fdef..04be937e4 100644 --- a/quantumclient/v2_0/client.py +++ b/quantumclient/v2_0/client.py @@ -132,7 +132,7 @@ class Client(object): tenant_name=TENANT_NAME, auth_url=KEYSTONE_URL) - >>> nets = quantum.list_nets() + >>> nets = quantum.list_networks() ... """ From 24a12125d103b88b6357022857af2c4ecd71344a Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Tue, 22 Jan 2013 21:27:08 -0800 Subject: [PATCH 21/29] Exception should raise with status code Fixes bug 1103330 Change-Id: Ia706a76429027a4627ea7d12eeee3badae8c16b5 --- quantumclient/v2_0/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quantumclient/v2_0/client.py b/quantumclient/v2_0/client.py index 04be937e4..6131cf8ea 100644 --- a/quantumclient/v2_0/client.py +++ b/quantumclient/v2_0/client.py @@ -73,7 +73,8 @@ def exception_handler_v20(status_code, error_content): if ex: raise ex else: - raise exceptions.QuantumClientException(message=error_dict) + raise exceptions.QuantumClientException(status_code=status_code, + message=error_dict) else: message = None if isinstance(error_content, dict): From 66d803901a99af2613edaf0819192ba8f5852a60 Mon Sep 17 00:00:00 2001 From: gongysh Date: Sat, 26 Jan 2013 09:00:52 +0800 Subject: [PATCH 22/29] Remove multiple white spaces Change-Id: I3715160a18cc2facdb8978ec97bf0f7ece1c3f05 --- quantumclient/tests/unit/test_casual_args.py | 4 ++-- quantumclient/tests/unit/test_cli20_floatingips.py | 4 ++-- quantumclient/tests/unit/test_cli20_subnet.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/quantumclient/tests/unit/test_casual_args.py b/quantumclient/tests/unit/test_casual_args.py index 4eb2b6622..0e34168c3 100644 --- a/quantumclient/tests/unit/test_casual_args.py +++ b/quantumclient/tests/unit/test_casual_args.py @@ -33,12 +33,12 @@ def test_default_bool(self): self.assertTrue(_mydict['my_bool']) def test_bool_true(self): - _specs = ['--my-bool', 'type=bool', 'true', '--arg1', 'value1'] + _specs = ['--my-bool', 'type=bool', 'true', '--arg1', 'value1'] _mydict = quantumV20.parse_args_to_dict(_specs) self.assertTrue(_mydict['my_bool']) def test_bool_false(self): - _specs = ['--my_bool', 'type=bool', 'false', '--arg1', 'value1'] + _specs = ['--my_bool', 'type=bool', 'false', '--arg1', 'value1'] _mydict = quantumV20.parse_args_to_dict(_specs) self.assertFalse(_mydict['my_bool']) diff --git a/quantumclient/tests/unit/test_cli20_floatingips.py b/quantumclient/tests/unit/test_cli20_floatingips.py index 7d0da0424..7b5912c00 100644 --- a/quantumclient/tests/unit/test_cli20_floatingips.py +++ b/quantumclient/tests/unit/test_cli20_floatingips.py @@ -108,7 +108,7 @@ def test_disassociate_ip(self): cmd = DisassociateFloatingIP(MyApp(sys.stdout), None) args = ['myid'] self._test_update_resource(resource, cmd, 'myid', - args, {"port_id": None} + args, {"port_id": None} ) def test_associate_ip(self): @@ -117,5 +117,5 @@ def test_associate_ip(self): cmd = AssociateFloatingIP(MyApp(sys.stdout), None) args = ['myid', 'portid'] self._test_update_resource(resource, cmd, 'myid', - args, {"port_id": "portid"} + args, {"port_id": "portid"} ) diff --git a/quantumclient/tests/unit/test_cli20_subnet.py b/quantumclient/tests/unit/test_cli20_subnet.py index ec4396fd5..997718bb2 100644 --- a/quantumclient/tests/unit/test_cli20_subnet.py +++ b/quantumclient/tests/unit/test_cli20_subnet.py @@ -51,7 +51,7 @@ def test_create_subnet_with_no_gateway(self): myid = 'myid' netid = 'netid' cidr = 'cidrvalue' - args = ['--no-gateway', netid, cidr] + args = ['--no-gateway', netid, cidr] position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] position_values = [4, netid, cidr, None] _str = self._test_create_resource(resource, cmd, name, myid, args, @@ -66,7 +66,7 @@ def test_create_subnet_with_bad_gateway_option(self): netid = 'netid' cidr = 'cidrvalue' gateway = 'gatewayvalue' - args = ['--gateway', gateway, '--no-gateway', netid, cidr] + args = ['--gateway', gateway, '--no-gateway', netid, cidr] position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] position_values = [4, netid, cidr, None] try: From 0feeaaaa300277d3755c80bc8fc5d6b2bcf12559 Mon Sep 17 00:00:00 2001 From: gongysh Date: Sun, 27 Jan 2013 22:51:17 +0800 Subject: [PATCH 23/29] Delete network with id in sample code using API. Bug #1106936 Change-Id: I6a7ee694a916af3f0268884ce28ca12e5efdc025 --- doc/source/index.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 85ca4009f..1dd7caefc 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,13 +3,17 @@ Python bindings to the OpenStack Network API In order to use the python quantum client directly, you must first obtain an auth token and identify which endpoint you wish to speak to. Once you have done so, you can use the API like so:: + >>> import logging >>> from quantumclient.quantum import client + >>> logging.basicConfig(level=logging.DEBUG) >>> quantum = client.Client('2.0', endpoint_url=OS_URL, token=OS_TOKEN) + >>> quantum.format = 'json' >>> network = {'name': 'mynetwork', 'admin_state_up': True} >>> quantum.create_network({'network':network}) >>> networks = quantum.list_networks(name='mynetwork') >>> print networks - >>> quantum.delete_network(name='mynetwork') + >>> network_id = networks['networks'][0]['id'] + >>> quantum.delete_network(network_id) Command-line Tool From f6f7bcfa1aad821515e82b9734c418da650682e4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 8 Jan 2013 06:26:00 +0000 Subject: [PATCH 24/29] Migrate from nose to testr Part of blueprint grizzly-testtools Change-Id: Ia53b0987b1e890a96b190f4b1a47dde4bf84fb6f --- .testr.conf | 4 ++++ HACKING.rst | 19 +++++++++++++++++++ setup.cfg | 4 ---- tools/test-requires | 14 +++++++------- tox.ini | 24 +++++++++++------------- 5 files changed, 41 insertions(+), 24 deletions(-) create mode 100644 .testr.conf diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 000000000..d356fcf1e --- /dev/null +++ b/.testr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./quantumclient/tests $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/HACKING.rst b/HACKING.rst index d99a07f9a..c7643238e 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -185,3 +185,22 @@ For every new feature, unit tests should be created that both test and bug that had no unit test, a new passing unit test should be added. If a submitted bug fix does have a unit test, be sure to add a new one that fails without the patch and passes with the patch. + +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/setup.cfg b/setup.cfg index 6d19c21a9..14dcb5c8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,10 +3,6 @@ all_files = 1 build-dir = doc/build source-dir = doc/source -[nosetests] -verbosity = 2 -detailed-errors = 1 - [egg_info] tag_build = tag_date = 0 diff --git a/tools/test-requires b/tools/test-requires index 8b02c3b54..b82e805ed 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,11 +1,11 @@ -distribute>=0.6.24 cliff-tablib>=1.0 +coverage +discover +distribute>=0.6.24 +fixtures>=0.3.12 mox -nose -nose-exclude -nosexcover -openstack.nose_plugin -nosehtmloutput pep8 +python-subunit sphinx>=1.1.2 - +testrepository>=0.0.13 +testtools>=0.9.22 diff --git a/tox.ini b/tox.ini index fce1185cf..c9d960819 100644 --- a/tox.ini +++ b/tox.ini @@ -3,23 +3,21 @@ envlist = py26,py27,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} - NOSE_WITH_OPENSTACK=1 - NOSE_OPENSTACK_COLOR=1 - NOSE_OPENSTACK_RED=0.05 - NOSE_OPENSTACK_YELLOW=0.025 - NOSE_OPENSTACK_SHOW_ELAPSED=1 - NOSE_OPENSTACK_STDOUT=1 -deps = -r{toxinidir}/tools/test-requires -commands = nosetests {posargs} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C -[tox:jenkins] -downloadcache = ~/cache/pip +deps = -r{toxinidir}/tools/test-requires +commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc . -[testenv:cover] -setenv = NOSE_WITH_COVERAGE=1 - [testenv:venv] commands = {posargs} + +[testenv:cover] +commands = python setup.py testr --coverage --testr-args='{posargs}' + +[tox:jenkins] +downloadcache = ~/cache/pip From 3655bd1447664221ed81428197b30ef60f782d53 Mon Sep 17 00:00:00 2001 From: He Jie Xu Date: Sat, 19 Jan 2013 21:39:40 +0800 Subject: [PATCH 25/29] Remove gettext.install from quantumclient.__init__ fix bug 1097628 Change-Id: Ic6f2dfb4593fa507fb788786616bff6af3cbb896 --- quantumclient/__init__.py | 6 ------ quantumclient/common/__init__.py | 8 ++++++++ quantumclient/common/exceptions.py | 2 ++ quantumclient/shell.py | 2 +- quantumclient/tests/unit/__init__.py | 22 ++++++++++++++++++++++ quantumclient/v2_0/client.py | 1 + 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/quantumclient/__init__.py b/quantumclient/__init__.py index 5558fdb97..034d66eaf 100644 --- a/quantumclient/__init__.py +++ b/quantumclient/__init__.py @@ -15,9 +15,3 @@ # License for the specific language governing permissions and limitations # under the License. # @author: Tyler Smith, Cisco Systems - -import gettext - - -# gettext must be initialized before any quantumclient imports -gettext.install('quantumclient', unicode=1) diff --git a/quantumclient/common/__init__.py b/quantumclient/common/__init__.py index 7e695ff08..1415c50a3 100644 --- a/quantumclient/common/__init__.py +++ b/quantumclient/common/__init__.py @@ -14,3 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. # @author: Somik Behera, Nicira Networks, Inc. + +import gettext + +t = gettext.translation('quantumclient', fallback=True) + + +def _(msg): + return t.ugettext(msg) diff --git a/quantumclient/common/exceptions.py b/quantumclient/common/exceptions.py index 734a49894..22ac67a49 100644 --- a/quantumclient/common/exceptions.py +++ b/quantumclient/common/exceptions.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +from quantumclient.common import _ + """ Quantum base exception handling. """ diff --git a/quantumclient/shell.py b/quantumclient/shell.py index 36fb63da0..f8b3cd528 100644 --- a/quantumclient/shell.py +++ b/quantumclient/shell.py @@ -33,7 +33,6 @@ from quantumclient.common import utils -gettext.install('quantum', unicode=1) VERSION = '2.0' QUANTUM_API_VERSION = '2.0' @@ -501,6 +500,7 @@ def configure_logging(self): def main(argv=sys.argv[1:]): + gettext.install('quantumclient', unicode=1) try: return QuantumShell(QUANTUM_API_VERSION).run(argv) except exc.QuantumClientException: diff --git a/quantumclient/tests/unit/__init__.py b/quantumclient/tests/unit/__init__.py index e69de29bb..353303fb4 100644 --- a/quantumclient/tests/unit/__init__.py +++ b/quantumclient/tests/unit/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2013 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import gettext + +# Because we installed '_' for quantum cli in shell.py, this help unittest +# have definition of '_' +gettext.install('quantumclient', unicode=1) diff --git a/quantumclient/v2_0/client.py b/quantumclient/v2_0/client.py index 6131cf8ea..8339d199d 100644 --- a/quantumclient/v2_0/client.py +++ b/quantumclient/v2_0/client.py @@ -21,6 +21,7 @@ import urllib from quantumclient.client import HTTPClient +from quantumclient.common import _ from quantumclient.common import exceptions from quantumclient.common.serializer import Serializer From 4fe469c28282200cd851b68dceb3dc972fdf2ccd Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Thu, 31 Jan 2013 19:54:11 -0800 Subject: [PATCH 26/29] Allow ability to remove security groups from ports This commit adds the option --no-security-groups to port-update in order to remove security groups from a port. Fixes bug 1112089 Change-Id: I43a5cc2c8b443f2d34fffe66b442925a5ae312ac --- quantumclient/quantum/v2_0/__init__.py | 22 ++++++++++++--------- quantumclient/quantum/v2_0/port.py | 12 +++++++++++ quantumclient/tests/unit/test_cli20_port.py | 8 ++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/quantumclient/quantum/v2_0/__init__.py b/quantumclient/quantum/v2_0/__init__.py index 530b07a84..e1b07b09e 100644 --- a/quantumclient/quantum/v2_0/__init__.py +++ b/quantumclient/quantum/v2_0/__init__.py @@ -248,6 +248,12 @@ def format_output_data(self, data): elif v is None: data[self.resource][k] = '' + def add_known_arguments(self, parser): + pass + + def args2body(self, parsed_args): + return {} + class CreateCommand(QuantumCommand, show.ShowOne): """Create a resource for a given tenant @@ -269,12 +275,6 @@ def get_parser(self, prog_name): self.add_known_arguments(parser) return parser - def add_known_arguments(self, parser): - pass - - def args2body(self, parsed_args): - return {} - def get_data(self, parsed_args): self.log.debug('get_data(%s)' % parsed_args) quantum_client = self.get_client() @@ -312,6 +312,7 @@ def get_parser(self, prog_name): help='ID or name of %s to update' % self.resource) add_extra_argument(parser, 'value_specs', 'new values for the %s' % self.resource) + self.add_known_arguments(parser) return parser def run(self, parsed_args): @@ -319,16 +320,19 @@ def run(self, parsed_args): quantum_client = self.get_client() quantum_client.format = parsed_args.request_format value_specs = parsed_args.value_specs - if not value_specs: + dict_args = self.args2body(parsed_args).get(self.resource, {}) + dict_specs = parse_args_to_dict(value_specs) + body = {self.resource: dict(dict_args.items() + + dict_specs.items())} + if not body[self.resource]: raise exceptions.CommandError( "Must specify new values to update %s" % self.resource) - data = {self.resource: parse_args_to_dict(value_specs)} _id = find_resourceid_by_name_or_id(quantum_client, self.resource, parsed_args.id) obj_updator = getattr(quantum_client, "update_%s" % self.resource) - obj_updator(_id, data) + obj_updator(_id, body) print >>self.app.stdout, ( _('Updated %(resource)s: %(id)s') % {'id': parsed_args.id, 'resource': self.resource}) diff --git a/quantumclient/quantum/v2_0/port.py b/quantumclient/quantum/v2_0/port.py index 98661b57d..448738848 100644 --- a/quantumclient/quantum/v2_0/port.py +++ b/quantumclient/quantum/v2_0/port.py @@ -161,3 +161,15 @@ class UpdatePort(UpdateCommand): resource = 'port' log = logging.getLogger(__name__ + '.UpdatePort') + + def add_known_arguments(self, parser): + parser.add_argument( + '--no-security-groups', + default=False, action='store_true', + help='remove security groups from port') + + def args2body(self, parsed_args): + body = {'port': {}} + if parsed_args.no_security_groups: + body['port'].update({'security_groups': None}) + return body diff --git a/quantumclient/tests/unit/test_cli20_port.py b/quantumclient/tests/unit/test_cli20_port.py index bd776d4a0..7324d2324 100644 --- a/quantumclient/tests/unit/test_cli20_port.py +++ b/quantumclient/tests/unit/test_cli20_port.py @@ -230,6 +230,14 @@ def test_update_port(self): {'name': 'myname', 'tags': ['a', 'b'], } ) + def test_update_port_security_group_off(self): + """Update port: --no-security-groups myid.""" + resource = 'port' + cmd = UpdatePort(MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['--no-security-groups', 'myid'], + {'security_groups': None}) + def test_show_port(self): """Show port: --fields id --fields name myid.""" resource = 'port' From 163765976897ba3991269059a7b09e5904d516ae Mon Sep 17 00:00:00 2001 From: gongysh Date: Fri, 18 Jan 2013 23:37:01 +0800 Subject: [PATCH 27/29] Support XML request format blueprint quantum-client-xml Change-Id: I9db8ea7c395909def00d6f25c9c1a98c07fdde68 --- openstack-common.conf | 2 +- quantum_test.sh | 75 ++-- quantumclient/common/constants.py | 40 ++ quantumclient/common/serializer.py | 465 +++++++++++++++----- quantumclient/openstack/common/jsonutils.py | 148 +++++++ quantumclient/openstack/common/timeutils.py | 164 +++++++ quantumclient/v2_0/client.py | 64 +-- tools/pip-requires | 5 +- tox.ini | 2 +- 9 files changed, 789 insertions(+), 176 deletions(-) create mode 100644 quantumclient/common/constants.py create mode 100644 quantumclient/openstack/common/jsonutils.py create mode 100644 quantumclient/openstack/common/timeutils.py diff --git a/openstack-common.conf b/openstack-common.conf index 35d48d06e..d52611484 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=setup +modules=jsonutils,setup,timeutils # The base module to hold the copy of openstack.common base=quantumclient diff --git a/quantum_test.sh b/quantum_test.sh index e19600bec..b42299beb 100755 --- a/quantum_test.sh +++ b/quantum_test.sh @@ -14,11 +14,12 @@ else NOAUTH= fi +FORMAT=" --request-format xml" # test the CRUD of network network=mynet1 -quantum net-create $NOAUTH $network || die "fail to create network $network" -temp=`quantum net-list -- --name $network --fields id | wc -l` +quantum net-create $FORMAT $NOAUTH $network || die "fail to create network $network" +temp=`quantum net-list $FORMAT -- --name $network --fields id | wc -l` echo $temp if [ $temp -ne 5 ]; then die "networks with name $network is not unique or found" @@ -26,102 +27,102 @@ fi network_id=`quantum net-list -- --name $network --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` echo "ID of network with name $network is $network_id" -quantum net-show $network || die "fail to show network $network" -quantum net-show $network_id || die "fail to show network $network_id" +quantum net-show $FORMAT $network || die "fail to show network $network" +quantum net-show $FORMAT $network_id || die "fail to show network $network_id" -quantum net-update $network --admin_state_up False || die "fail to update network $network" -quantum net-update $network_id --admin_state_up True || die "fail to update network $network_id" +quantum net-update $FORMAT $network --admin_state_up False || die "fail to update network $network" +quantum net-update $FORMAT $network_id --admin_state_up True || die "fail to update network $network_id" -quantum net-list -c id -- --id fakeid || die "fail to list networks with column selection on empty list" +quantum net-list $FORMAT -c id -- --id fakeid || die "fail to list networks with column selection on empty list" # test the CRUD of subnet subnet=mysubnet1 cidr=10.0.1.3/24 -quantum subnet-create $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet" -tempsubnet=`quantum subnet-list -- --name $subnet --fields id | wc -l` +quantum subnet-create $FORMAT $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet" +tempsubnet=`quantum subnet-list $FORMAT -- --name $subnet --fields id | wc -l` echo $tempsubnet if [ $tempsubnet -ne 5 ]; then die "subnets with name $subnet is not unique or found" fi -subnet_id=`quantum subnet-list -- --name $subnet --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` +subnet_id=`quantum subnet-list $FORMAT -- --name $subnet --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` echo "ID of subnet with name $subnet is $subnet_id" -quantum subnet-show $subnet || die "fail to show subnet $subnet" -quantum subnet-show $subnet_id || die "fail to show subnet $subnet_id" +quantum subnet-show $FORMAT $subnet || die "fail to show subnet $subnet" +quantum subnet-show $FORMAT $subnet_id || die "fail to show subnet $subnet_id" -quantum subnet-update $subnet --dns_namesevers host1 || die "fail to update subnet $subnet" -quantum subnet-update $subnet_id --dns_namesevers host2 || die "fail to update subnet $subnet_id" +quantum subnet-update $FORMAT $subnet --dns_namesevers host1 || die "fail to update subnet $subnet" +quantum subnet-update $FORMAT $subnet_id --dns_namesevers host2 || die "fail to update subnet $subnet_id" # test the crud of ports port=myport1 -quantum port-create $NOAUTH $network --name $port || die "fail to create port $port" -tempport=`quantum port-list -- --name $port --fields id | wc -l` +quantum port-create $FORMAT $NOAUTH $network --name $port || die "fail to create port $port" +tempport=`quantum port-list $FORMAT -- --name $port --fields id | wc -l` echo $tempport if [ $tempport -ne 5 ]; then die "ports with name $port is not unique or found" fi -port_id=`quantum port-list -- --name $port --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` +port_id=`quantum port-list $FORMAT -- --name $port --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` echo "ID of port with name $port is $port_id" -quantum port-show $port || die "fail to show port $port" -quantum port-show $port_id || die "fail to show port $port_id" +quantum port-show $FORMAT $port || die "fail to show port $port" +quantum port-show $FORMAT $port_id || die "fail to show port $port_id" -quantum port-update $port --device_id deviceid1 || die "fail to update port $port" -quantum port-update $port_id --device_id deviceid2 || die "fail to update port $port_id" +quantum port-update $FORMAT $port --device_id deviceid1 || die "fail to update port $port" +quantum port-update $FORMAT $port_id --device_id deviceid2 || die "fail to update port $port_id" # test quota commands RUD DEFAULT_NETWORKS=10 DEFAULT_PORTS=50 tenant_id=tenant_a tenant_id_b=tenant_b -quantum quota-update --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id" -quantum quota-update --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id" -networks=`quantum quota-list -c network -c tenant_id | grep $tenant_id | awk '{print $2}'` +quantum quota-update $FORMAT --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id" +quantum quota-update $FORMAT --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id" +networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id | awk '{print $2}'` if [ $networks -ne 30 ]; then die "networks quota should be 30" fi -networks=`quantum quota-list -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'` +networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'` if [ $networks -ne 20 ]; then die "networks quota should be 20" fi -networks=`quantum quota-show --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` +networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` if [ $networks -ne 30 ]; then die "networks quota should be 30" fi -quantum quota-delete --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id" -networks=`quantum quota-show --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` +quantum quota-delete $FORMAT --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id" +networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` if [ $networks -ne $DEFAULT_NETWORKS ]; then die "networks quota should be $DEFAULT_NETWORKS" fi # update self if [ "t$NOAUTH" = "t" ]; then # with auth - quantum quota-update --port 99 || die "fail to update quota for self" - ports=`quantum quota-show | grep port | awk -F'|' '{print $3}'` + quantum quota-update $FORMAT --port 99 || die "fail to update quota for self" + ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'` if [ $ports -ne 99 ]; then die "ports quota should be 99" fi - ports=`quantum quota-list -c port | grep 99 | awk '{print $2}'` + ports=`quantum quota-list $FORMAT -c port | grep 99 | awk '{print $2}'` if [ $ports -ne 99 ]; then die "ports quota should be 99" fi - quantum quota-delete || die "fail to delete quota for tenant self" - ports=`quantum quota-show | grep port | awk -F'|' '{print $3}'` + quantum quota-delete $FORMAT || die "fail to delete quota for tenant self" + ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'` if [ $ports -ne $DEFAULT_PORTS ]; then die "ports quota should be $DEFAULT_PORTS" fi else # without auth - quantum quota-update --port 100 + quantum quota-update $FORMAT --port 100 if [ $? -eq 0 ]; then die "without valid context on server, quota update command should fail." fi - quantum quota-show + quantum quota-show $FORMAT if [ $? -eq 0 ]; then die "without valid context on server, quota show command should fail." fi - quantum quota-delete + quantum quota-delete $FORMAT if [ $? -eq 0 ]; then die "without valid context on server, quota delete command should fail." fi - quantum quota-list || die "fail to update quota for self" + quantum quota-list $FORMAT || die "fail to update quota for self" fi diff --git a/quantumclient/common/constants.py b/quantumclient/common/constants.py new file mode 100644 index 000000000..572baa941 --- /dev/null +++ b/quantumclient/common/constants.py @@ -0,0 +1,40 @@ +# Copyright (c) 2012 OpenStack, LLC. +# +# 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. + + +EXT_NS = '_extension_ns' +XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0' +XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" +XSI_ATTR = "xsi:nil" +XSI_NIL_ATTR = "xmlns:xsi" +TYPE_XMLNS = "xmlns:quantum" +TYPE_ATTR = "quantum:type" +VIRTUAL_ROOT_KEY = "_v_root" + +TYPE_BOOL = "bool" +TYPE_INT = "int" +TYPE_LONG = "long" +TYPE_FLOAT = "float" +TYPE_LIST = "list" +TYPE_DICT = "dict" + +PLURALS = {'networks': 'network', + 'ports': 'port', + 'subnets': 'subnet', + 'dns_nameservers': 'dns_nameserver', + 'host_routes': 'host_route', + 'allocation_pools': 'allocation_pool', + 'fixed_ips': 'fixed_ip', + 'extensions': 'extension'} diff --git a/quantumclient/common/serializer.py b/quantumclient/common/serializer.py index 6e914673a..7eba93069 100644 --- a/quantumclient/common/serializer.py +++ b/quantumclient/common/serializer.py @@ -1,7 +1,353 @@ -from xml.dom import minidom +# Copyright 2013 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +### +### Codes from quantum wsgi +### + +import logging + +from xml.etree import ElementTree as etree +from xml.parsers import expat + +from quantumclient.common import constants from quantumclient.common import exceptions as exception -from quantumclient.common import utils +from quantumclient.openstack.common import jsonutils + +LOG = logging.getLogger(__name__) + + +class ActionDispatcher(object): + """Maps method name to local methods through action name.""" + + def dispatch(self, *args, **kwargs): + """Find and call local method.""" + action = kwargs.pop('action', 'default') + action_method = getattr(self, str(action), self.default) + return action_method(*args, **kwargs) + + def default(self, data): + raise NotImplementedError() + + +class DictSerializer(ActionDispatcher): + """Default request body serialization""" + + def serialize(self, data, action='default'): + return self.dispatch(data, action=action) + + def default(self, data): + return "" + + +class JSONDictSerializer(DictSerializer): + """Default JSON request body serialization""" + + def default(self, data): + return jsonutils.dumps(data) + + +class XMLDictSerializer(DictSerializer): + + def __init__(self, metadata=None, xmlns=None): + """ + :param metadata: information needed to deserialize xml into + a dictionary. + :param xmlns: XML namespace to include with serialized xml + """ + super(XMLDictSerializer, self).__init__() + self.metadata = metadata or {} + if not xmlns: + xmlns = self.metadata.get('xmlns') + if not xmlns: + xmlns = constants.XML_NS_V20 + self.xmlns = xmlns + + def default(self, data): + # We expect data to contain a single key which is the XML root or + # non root + try: + key_len = data and len(data.keys()) or 0 + if (key_len == 1): + root_key = data.keys()[0] + root_value = data[root_key] + else: + root_key = constants.VIRTUAL_ROOT_KEY + root_value = data + doc = etree.Element("_temp_root") + used_prefixes = [] + self._to_xml_node(doc, self.metadata, root_key, + root_value, used_prefixes) + return self.to_xml_string(list(doc)[0], used_prefixes) + except AttributeError as e: + LOG.exception(str(e)) + return '' + + def __call__(self, data): + # Provides a migration path to a cleaner WSGI layer, this + # "default" stuff and extreme extensibility isn't being used + # like originally intended + return self.default(data) + + def to_xml_string(self, node, used_prefixes, has_atom=False): + self._add_xmlns(node, used_prefixes, has_atom) + return etree.tostring(node, encoding='UTF-8') + + #NOTE (ameade): the has_atom should be removed after all of the + # xml serializers and view builders have been updated to the current + # spec that required all responses include the xmlns:atom, the has_atom + # flag is to prevent current tests from breaking + def _add_xmlns(self, node, used_prefixes, has_atom=False): + node.set('xmlns', self.xmlns) + node.set(constants.TYPE_XMLNS, self.xmlns) + if has_atom: + node.set('xmlns:atom', "http://www.w3.org/2005/Atom") + node.set(constants.XSI_NIL_ATTR, constants.XSI_NAMESPACE) + ext_ns = self.metadata.get(constants.EXT_NS, {}) + for prefix in used_prefixes: + if prefix in ext_ns: + node.set('xmlns:' + prefix, ext_ns[prefix]) + + def _to_xml_node(self, parent, metadata, nodename, data, used_prefixes): + """Recursive method to convert data members to XML nodes.""" + result = etree.SubElement(parent, nodename) + if ":" in nodename: + used_prefixes.append(nodename.split(":", 1)[0]) + #TODO(bcwaldon): accomplish this without a type-check + if isinstance(data, list): + if not data: + result.set( + constants.TYPE_ATTR, + constants.TYPE_LIST) + return result + singular = metadata.get('plurals', {}).get(nodename, None) + if singular is None: + if nodename.endswith('s'): + singular = nodename[:-1] + else: + singular = 'item' + for item in data: + self._to_xml_node(result, metadata, singular, item, + used_prefixes) + #TODO(bcwaldon): accomplish this without a type-check + elif isinstance(data, dict): + if not data: + result.set( + constants.TYPE_ATTR, + constants.TYPE_DICT) + return result + attrs = metadata.get('attributes', {}).get(nodename, {}) + for k, v in data.items(): + if k in attrs: + result.set(k, str(v)) + else: + self._to_xml_node(result, metadata, k, v, + used_prefixes) + elif data is None: + result.set(constants.XSI_ATTR, 'true') + else: + if isinstance(data, bool): + result.set( + constants.TYPE_ATTR, + constants.TYPE_BOOL) + elif isinstance(data, int): + result.set( + constants.TYPE_ATTR, + constants.TYPE_INT) + elif isinstance(data, long): + result.set( + constants.TYPE_ATTR, + constants.TYPE_LONG) + elif isinstance(data, float): + result.set( + constants.TYPE_ATTR, + constants.TYPE_FLOAT) + LOG.debug(_("Data %(data)s type is %(type)s"), + {'data': data, + 'type': type(data)}) + result.text = str(data) + return result + + def _create_link_nodes(self, xml_doc, links): + link_nodes = [] + for link in links: + link_node = xml_doc.createElement('atom:link') + link_node.set('rel', link['rel']) + link_node.set('href', link['href']) + if 'type' in link: + link_node.set('type', link['type']) + link_nodes.append(link_node) + return link_nodes + + +class TextDeserializer(ActionDispatcher): + """Default request body deserialization""" + + def deserialize(self, datastring, action='default'): + return self.dispatch(datastring, action=action) + + def default(self, datastring): + return {} + + +class JSONDeserializer(TextDeserializer): + + def _from_json(self, datastring): + try: + return jsonutils.loads(datastring) + except ValueError: + msg = _("Cannot understand JSON") + raise exception.MalformedRequestBody(reason=msg) + + def default(self, datastring): + return {'body': self._from_json(datastring)} + + +class XMLDeserializer(TextDeserializer): + + def __init__(self, metadata=None): + """ + :param metadata: information needed to deserialize xml into + a dictionary. + """ + super(XMLDeserializer, self).__init__() + self.metadata = metadata or {} + xmlns = self.metadata.get('xmlns') + if not xmlns: + xmlns = constants.XML_NS_V20 + self.xmlns = xmlns + + def _get_key(self, tag): + tags = tag.split("}", 1) + if len(tags) == 2: + ns = tags[0][1:] + bare_tag = tags[1] + ext_ns = self.metadata.get(constants.EXT_NS, {}) + if ns == self.xmlns: + return bare_tag + for prefix, _ns in ext_ns.items(): + if ns == _ns: + return prefix + ":" + bare_tag + else: + return tag + + def _from_xml(self, datastring): + if datastring is None: + return None + plurals = set(self.metadata.get('plurals', {})) + try: + node = etree.fromstring(datastring) + result = self._from_xml_node(node, plurals) + root_tag = self._get_key(node.tag) + if root_tag == constants.VIRTUAL_ROOT_KEY: + return result + else: + return {root_tag: result} + except Exception as e: + parseError = False + # Python2.7 + if (hasattr(etree, 'ParseError') and + isinstance(e, getattr(etree, 'ParseError'))): + parseError = True + # Python2.6 + elif isinstance(e, expat.ExpatError): + parseError = True + if parseError: + msg = _("Cannot understand XML") + raise exception.MalformedRequestBody(reason=msg) + else: + raise + + def _from_xml_node(self, node, listnames): + """Convert a minidom node to a simple Python type. + + :param listnames: list of XML node names whose subnodes should + be considered list items. + + """ + attrNil = node.get(str(etree.QName(constants.XSI_NAMESPACE, "nil"))) + attrType = node.get(str(etree.QName( + self.metadata.get('xmlns'), "type"))) + if (attrNil and attrNil.lower() == 'true'): + return None + elif not len(node) and not node.text: + if (attrType and attrType == constants.TYPE_DICT): + return {} + elif (attrType and attrType == constants.TYPE_LIST): + return [] + else: + return '' + elif (len(node) == 0 and node.text): + converters = {constants.TYPE_BOOL: + lambda x: x.lower() == 'true', + constants.TYPE_INT: + lambda x: int(x), + constants.TYPE_LONG: + lambda x: long(x), + constants.TYPE_FLOAT: + lambda x: float(x)} + if attrType and attrType in converters: + return converters[attrType](node.text) + else: + return node.text + elif self._get_key(node.tag) in listnames: + return [self._from_xml_node(n, listnames) for n in node] + else: + result = dict() + for attr in node.keys(): + if (attr == 'xmlns' or + attr.startswith('xmlns:') or + attr == constants.XSI_ATTR or + attr == constants.TYPE_ATTR): + continue + result[self._get_key(attr)] = node.get[attr] + children = list(node) + for child in children: + result[self._get_key(child.tag)] = self._from_xml_node( + child, listnames) + return result + + def find_first_child_named(self, parent, name): + """Search a nodes children for the first child with a given name""" + for node in parent.childNodes: + if node.nodeName == name: + return node + return None + + def find_children_named(self, parent, name): + """Return all of a nodes children who have the given name""" + for node in parent.childNodes: + if node.nodeName == name: + yield node + + def extract_text(self, node): + """Get the text field contained by the given node""" + if len(node.childNodes) == 1: + child = node.childNodes[0] + if child.nodeType == child.TEXT_NODE: + return child.nodeValue + return "" + + def default(self, datastring): + return {'body': self._from_xml(datastring)} + + def __call__(self, datastring): + # Adding a migration path to allow us to remove unncessary classes + return self.default(datastring) # NOTE(maru): this class is duplicated from quantum.wsgi @@ -20,8 +366,8 @@ def __init__(self, metadata=None, default_xmlns=None): def _get_serialize_handler(self, content_type): handlers = { - 'application/json': self._to_json, - 'application/xml': self._to_xml, + 'application/json': JSONDictSerializer(), + 'application/xml': XMLDictSerializer(self.metadata), } try: @@ -31,7 +377,7 @@ def _get_serialize_handler(self, content_type): def serialize(self, data, content_type): """Serialize a dictionary into the specified content type.""" - return self._get_serialize_handler(content_type)(data) + return self._get_serialize_handler(content_type).serialize(data) def deserialize(self, datastring, content_type): """Deserialize a string to a dictionary. @@ -39,117 +385,16 @@ def deserialize(self, datastring, content_type): The string must be in the format of a supported MIME type. """ - try: - return self.get_deserialize_handler(content_type)(datastring) - except Exception: - raise exception.MalformedResponseBody( - reason="Unable to deserialize response body") + return self.get_deserialize_handler(content_type).deserialize( + datastring) def get_deserialize_handler(self, content_type): handlers = { - 'application/json': self._from_json, - 'application/xml': self._from_xml, + 'application/json': JSONDeserializer(), + 'application/xml': XMLDeserializer(self.metadata), } try: return handlers[content_type] except Exception: raise exception.InvalidContentType(content_type=content_type) - - def _from_json(self, datastring): - return utils.loads(datastring) - - def _from_xml(self, datastring): - xmldata = self.metadata.get('application/xml', {}) - plurals = set(xmldata.get('plurals', {})) - node = minidom.parseString(datastring).childNodes[0] - return {node.nodeName: self._from_xml_node(node, plurals)} - - def _from_xml_node(self, node, listnames): - """Convert a minidom node to a simple Python type. - - listnames is a collection of names of XML nodes whose subnodes should - be considered list items. - - """ - if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: - return node.childNodes[0].nodeValue - elif node.nodeName in listnames: - return [self._from_xml_node(n, listnames) - for n in node.childNodes if n.nodeType != node.TEXT_NODE] - else: - result = dict() - for attr in node.attributes.keys(): - result[attr] = node.attributes[attr].nodeValue - for child in node.childNodes: - if child.nodeType != node.TEXT_NODE: - result[child.nodeName] = self._from_xml_node(child, - listnames) - return result - - def _to_json(self, data): - return utils.dumps(data) - - def _to_xml(self, data): - metadata = self.metadata.get('application/xml', {}) - # We expect data to contain a single key which is the XML root. - root_key = data.keys()[0] - doc = minidom.Document() - node = self._to_xml_node(doc, metadata, root_key, data[root_key]) - - xmlns = node.getAttribute('xmlns') - if not xmlns and self.default_xmlns: - node.setAttribute('xmlns', self.default_xmlns) - - return node.toprettyxml(indent='', newl='') - - def _to_xml_node(self, doc, metadata, nodename, data): - """Recursive method to convert data members to XML nodes.""" - result = doc.createElement(nodename) - - # Set the xml namespace if one is specified - # TODO(justinsb): We could also use prefixes on the keys - xmlns = metadata.get('xmlns', None) - if xmlns: - result.setAttribute('xmlns', xmlns) - if type(data) is list: - collections = metadata.get('list_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for item in data: - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(item)) - result.appendChild(node) - return result - singular = metadata.get('plurals', {}).get(nodename, None) - if singular is None: - if nodename.endswith('s'): - singular = nodename[:-1] - else: - singular = 'item' - for item in data: - node = self._to_xml_node(doc, metadata, singular, item) - result.appendChild(node) - elif type(data) is dict: - collections = metadata.get('dict_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for k, v in data.items(): - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(k)) - text = doc.createTextNode(str(v)) - node.appendChild(text) - result.appendChild(node) - return result - attrs = metadata.get('attributes', {}).get(nodename, {}) - for k, v in data.items(): - if k in attrs: - result.setAttribute(k, str(v)) - else: - node = self._to_xml_node(doc, metadata, k, v) - result.appendChild(node) - else: - # Type is atom. - node = doc.createTextNode(str(data)) - result.appendChild(node) - return result diff --git a/quantumclient/openstack/common/jsonutils.py b/quantumclient/openstack/common/jsonutils.py new file mode 100644 index 000000000..ad76e0655 --- /dev/null +++ b/quantumclient/openstack/common/jsonutils.py @@ -0,0 +1,148 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara +# 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. + +''' +JSON related utilities. + +This module provides a few things: + + 1) A handy function for getting an object down to something that can be + JSON serialized. See to_primitive(). + + 2) Wrappers around loads() and dumps(). The dumps() wrapper will + automatically use to_primitive() for you if needed. + + 3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson + is available. +''' + + +import datetime +import inspect +import itertools +import json +import xmlrpclib + +from quantumclient.openstack.common import timeutils + + +def to_primitive(value, convert_instances=False, level=0): + """Convert a complex object into primitives. + + Handy for JSON serialization. We can optionally handle instances, + but since this is a recursive function, we could have cyclical + data structures. + + To handle cyclical data structures we could track the actual objects + visited in a set, but not all objects are hashable. Instead we just + track the depth of the object inspections and don't go too deep. + + Therefore, convert_instances=True is lossy ... be aware. + + """ + nasty = [inspect.ismodule, inspect.isclass, inspect.ismethod, + inspect.isfunction, inspect.isgeneratorfunction, + inspect.isgenerator, inspect.istraceback, inspect.isframe, + inspect.iscode, inspect.isbuiltin, inspect.isroutine, + inspect.isabstract] + for test in nasty: + if test(value): + return unicode(value) + + # value of itertools.count doesn't get caught by inspects + # above and results in infinite loop when list(value) is called. + if type(value) == itertools.count: + return unicode(value) + + # FIXME(vish): Workaround for LP bug 852095. Without this workaround, + # tests that raise an exception in a mocked method that + # has a @wrap_exception with a notifier will fail. If + # we up the dependency to 0.5.4 (when it is released) we + # can remove this workaround. + if getattr(value, '__module__', None) == 'mox': + return 'mock' + + if level > 3: + return '?' + + # The try block may not be necessary after the class check above, + # but just in case ... + try: + # It's not clear why xmlrpclib created their own DateTime type, but + # for our purposes, make it a datetime type which is explicitly + # handled + if isinstance(value, xmlrpclib.DateTime): + value = datetime.datetime(*tuple(value.timetuple())[:6]) + + if isinstance(value, (list, tuple)): + o = [] + for v in value: + o.append(to_primitive(v, convert_instances=convert_instances, + level=level)) + return o + elif isinstance(value, dict): + o = {} + for k, v in value.iteritems(): + o[k] = to_primitive(v, convert_instances=convert_instances, + level=level) + return o + elif isinstance(value, datetime.datetime): + return timeutils.strtime(value) + elif hasattr(value, 'iteritems'): + return to_primitive(dict(value.iteritems()), + convert_instances=convert_instances, + level=level + 1) + elif hasattr(value, '__iter__'): + return to_primitive(list(value), + convert_instances=convert_instances, + level=level) + elif convert_instances and hasattr(value, '__dict__'): + # Likely an instance of something. Watch for cycles. + # Ignore class member vars. + return to_primitive(value.__dict__, + convert_instances=convert_instances, + level=level + 1) + else: + return value + except TypeError: + # Class objects are tricky since they may define something like + # __iter__ defined but it isn't callable as list(). + return unicode(value) + + +def dumps(value, default=to_primitive, **kwargs): + return json.dumps(value, default=default, **kwargs) + + +def loads(s): + return json.loads(s) + + +def load(s): + return json.load(s) + + +try: + import anyjson +except ImportError: + pass +else: + anyjson._modules.append((__name__, 'dumps', TypeError, + 'loads', ValueError, 'load')) + anyjson.force_implementation(__name__) diff --git a/quantumclient/openstack/common/timeutils.py b/quantumclient/openstack/common/timeutils.py new file mode 100644 index 000000000..0f346087f --- /dev/null +++ b/quantumclient/openstack/common/timeutils.py @@ -0,0 +1,164 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +""" +Time related utilities and helper functions. +""" + +import calendar +import datetime + +import iso8601 + + +TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" +PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" + + +def isotime(at=None): + """Stringify time in ISO 8601 format""" + if not at: + at = utcnow() + str = at.strftime(TIME_FORMAT) + tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' + str += ('Z' if tz == 'UTC' else tz) + return str + + +def parse_isotime(timestr): + """Parse time from ISO 8601 format""" + try: + return iso8601.parse_date(timestr) + except iso8601.ParseError as e: + raise ValueError(e.message) + except TypeError as e: + raise ValueError(e.message) + + +def strtime(at=None, fmt=PERFECT_TIME_FORMAT): + """Returns formatted utcnow.""" + if not at: + at = utcnow() + return at.strftime(fmt) + + +def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): + """Turn a formatted time back into a datetime.""" + return datetime.datetime.strptime(timestr, fmt) + + +def normalize_time(timestamp): + """Normalize time in arbitrary timezone to UTC naive object""" + offset = timestamp.utcoffset() + if offset is None: + return timestamp + return timestamp.replace(tzinfo=None) - offset + + +def is_older_than(before, seconds): + """Return True if before is older than seconds.""" + if isinstance(before, basestring): + before = parse_strtime(before).replace(tzinfo=None) + return utcnow() - before > datetime.timedelta(seconds=seconds) + + +def is_newer_than(after, seconds): + """Return True if after is newer than seconds.""" + if isinstance(after, basestring): + after = parse_strtime(after).replace(tzinfo=None) + return after - utcnow() > datetime.timedelta(seconds=seconds) + + +def utcnow_ts(): + """Timestamp version of our utcnow function.""" + return calendar.timegm(utcnow().timetuple()) + + +def utcnow(): + """Overridable version of utils.utcnow.""" + if utcnow.override_time: + try: + return utcnow.override_time.pop(0) + except AttributeError: + return utcnow.override_time + return datetime.datetime.utcnow() + + +utcnow.override_time = None + + +def set_time_override(override_time=datetime.datetime.utcnow()): + """ + Override utils.utcnow to return a constant time or a list thereof, + one at a time. + """ + utcnow.override_time = override_time + + +def advance_time_delta(timedelta): + """Advance overridden time using a datetime.timedelta.""" + assert(not utcnow.override_time is None) + try: + for dt in utcnow.override_time: + dt += timedelta + except TypeError: + utcnow.override_time += timedelta + + +def advance_time_seconds(seconds): + """Advance overridden time by seconds.""" + advance_time_delta(datetime.timedelta(0, seconds)) + + +def clear_time_override(): + """Remove the overridden time.""" + utcnow.override_time = None + + +def marshall_now(now=None): + """Make an rpc-safe datetime with microseconds. + + Note: tzinfo is stripped, but not required for relative times.""" + if not now: + now = utcnow() + return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, + minute=now.minute, second=now.second, + microsecond=now.microsecond) + + +def unmarshall_time(tyme): + """Unmarshall a datetime dict.""" + return datetime.datetime(day=tyme['day'], + month=tyme['month'], + year=tyme['year'], + hour=tyme['hour'], + minute=tyme['minute'], + second=tyme['second'], + microsecond=tyme['microsecond']) + + +def delta_seconds(before, after): + """ + Compute the difference in seconds between two date, time, or + datetime objects (as a float, to microsecond resolution). + """ + delta = after - before + try: + return delta.total_seconds() + except AttributeError: + return ((delta.days * 24 * 3600) + delta.seconds + + float(delta.microseconds) / (10 ** 6)) diff --git a/quantumclient/v2_0/client.py b/quantumclient/v2_0/client.py index 8339d199d..53b597da8 100644 --- a/quantumclient/v2_0/client.py +++ b/quantumclient/v2_0/client.py @@ -22,8 +22,9 @@ from quantumclient.client import HTTPClient from quantumclient.common import _ +from quantumclient.common import constants from quantumclient.common import exceptions -from quantumclient.common.serializer import Serializer +from quantumclient.common import serializer _logger = logging.getLogger(__name__) @@ -139,18 +140,6 @@ class Client(object): """ - #Metadata for deserializing xml - _serialization_metadata = { - "application/xml": { - "attributes": { - "network": ["id", "name"], - "port": ["id", "mac_address"], - "subnet": ["id", "prefix"]}, - "plurals": { - "networks": "network", - "ports": "port", - "subnets": "subnet", }, }, } - networks_path = "/networks" network_path = "/networks/%s" ports_path = "/ports" @@ -166,6 +155,33 @@ class Client(object): floatingips_path = "/floatingips" floatingip_path = "/floatingips/%s" + # API has no way to report plurals, so we have to hard code them + EXTED_PLURALS = {'routers': 'router', + 'floatingips': 'floatingip', + 'service_types': 'service_type', + 'service_definitions': 'service_definition', + 'security_groups': 'security_group', + 'security_group_rules': 'security_group_rule', + 'vips': 'vip', + 'pools': 'pool', + 'members': 'member', + 'health_monitors': 'health_monitor', + 'quotas': 'quota', + } + + 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 get_quotas_tenant(self, **_params): """Fetch tenant info in server's context for @@ -425,16 +441,14 @@ def __init__(self, **kwargs): def _handle_fault_response(self, status_code, response_body): # Create exception with HTTP status code and message - error_message = response_body - _logger.debug("Error message: %s", error_message) + _logger.debug("Error message: %s", response_body) # Add deserialized error message to exception arguments try: - des_error_body = Serializer().deserialize(error_message, - self.content_type()) + des_error_body = self.deserialize(response_body, status_code) except: # If unable to deserialized body it is probably not a # Quantum error - des_error_body = {'message': error_message} + des_error_body = {'message': response_body} # Raise the appropriate exception exception_handler_v20(status_code, des_error_body) @@ -475,7 +489,8 @@ def serialize(self, data): if data is None: return None elif type(data) is dict: - return Serializer().serialize(data, self.content_type()) + return serializer.Serializer( + self.get_attr_metadata()).serialize(data, self.content_type()) else: raise Exception("unable to serialize object of type = '%s'" % type(data)) @@ -486,17 +501,16 @@ def deserialize(self, data, status_code): """ if status_code == 204: return data - return Serializer(self._serialization_metadata).deserialize( - data, self.content_type()) + return serializer.Serializer(self.get_attr_metadata()).deserialize( + data, self.content_type())['body'] - def content_type(self, format=None): + def content_type(self, _format=None): """ Returns the mime-type for either 'xml' or 'json'. Defaults to the currently set format """ - if not format: - format = self.format - return "application/%s" % (format) + _format = _format or self.format + return "application/%s" % (_format) def retry_request(self, method, action, body=None, headers=None, params=None): diff --git a/tools/pip-requires b/tools/pip-requires index 2deb910ce..59ee8fa34 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,6 +1,7 @@ -cliff>=1.2.1 argparse +cliff>=1.2.1 httplib2 +iso8601 prettytable>=0.6.0 -simplejson pyparsing>=1.5.6,<2.0 +simplejson diff --git a/tox.ini b/tox.ini index c9d960819..dbb367d4d 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ deps = -r{toxinidir}/tools/test-requires commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] -commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc . +commands = pep8 --repeat --show-source --ignore=E125 --exclude=.venv,.tox,dist,doc . [testenv:venv] commands = {posargs} From 6e308a73c55c080ccae2b559d5f7603158d94eb5 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Tue, 5 Feb 2013 17:51:20 -0800 Subject: [PATCH 28/29] Stored the quantum commands list to the variable. By storing the quantum commands list to the variable, the subclass can depend the quantum shell to parse the command. Fixes: bug 1116837 Change-Id: I0a6f3226d326cf015e262e4ddf364d6f9a91d041 --- quantumclient/shell.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quantumclient/shell.py b/quantumclient/shell.py index f8b3cd528..f95507ef3 100644 --- a/quantumclient/shell.py +++ b/quantumclient/shell.py @@ -169,7 +169,8 @@ def __init__(self, apiversion): description=__doc__.strip(), version=VERSION, command_manager=CommandManager('quantum.cli'), ) - for k, v in COMMANDS[apiversion].items(): + 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 @@ -320,7 +321,7 @@ def run(self, argv): if arg == 'bash-completion': self._bash_completion() return 0 - if arg in COMMANDS[self.api_version]: + if arg in self.commands[self.api_version]: if command_pos == -1: command_pos = index elif arg in ('-h', '--help'): From ed10bb095b91aace2cbd0b0a366d4facb04e5ab3 Mon Sep 17 00:00:00 2001 From: Gary Kotton Date: Thu, 14 Feb 2013 12:04:26 +0000 Subject: [PATCH 29/29] Add exceptions messages for authentication Fixes bug 1125137 Change-Id: Id79d11fb515a7af3b4e5cbe87bc717880fdb268b --- quantumclient/common/exceptions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quantumclient/common/exceptions.py b/quantumclient/common/exceptions.py index 22ac67a49..1df133d7b 100644 --- a/quantumclient/common/exceptions.py +++ b/quantumclient/common/exceptions.py @@ -94,7 +94,7 @@ class Unauthorized(QuantumClientException): """ HTTP 401 - Unauthorized: bad credentials. """ - pass + message = _("Unauthorized: bad credentials.") class Forbidden(QuantumClientException): @@ -102,12 +102,13 @@ class Forbidden(QuantumClientException): HTTP 403 - Forbidden: your credentials don't give you access to this resource. """ - pass + message = _("Forbidden: your credentials don't give you access to this " + "resource.") class EndpointNotFound(QuantumClientException): """Could not find Service or Region in Service Catalog.""" - pass + message = _("Could not find Service or Region in Service Catalog.") class AmbiguousEndpoints(QuantumClientException):