osc-lib-2.0.0/0000775000175000017500000000000013607371310013064 5ustar zuulzuul00000000000000osc-lib-2.0.0/setup.cfg0000664000175000017500000000121413607371310014703 0ustar zuulzuul00000000000000[metadata] name = osc-lib summary = OpenStackClient Library description-file = README.rst author = OpenStack author-email = openstack-discuss@lists.openstack.org home-page = https://docs.openstack.org/osc-lib/latest/ classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 [files] packages = osc_lib [egg_info] tag_build = tag_date = 0 osc-lib-2.0.0/.coveragerc0000664000175000017500000000010413607371235015206 0ustar zuulzuul00000000000000[run] branch = True source = osc_lib [report] ignore_errors = True osc-lib-2.0.0/PKG-INFO0000664000175000017500000000677713607371310014202 0ustar zuulzuul00000000000000Metadata-Version: 1.1 Name: osc-lib Version: 2.0.0 Summary: OpenStackClient Library Home-page: https://docs.openstack.org/osc-lib/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ======= osc-lib ======= .. image:: https://img.shields.io/pypi/v/osc-lib.svg :target: https://pypi.org/project/osc-lib/ :alt: Latest Version OpenStackClient (aka OSC) is a command-line client for OpenStack. osc-lib is a package of common support modules for writing OSC plugins. * `PyPi`_ - package installation * `Online Documentation`_ * `Launchpad project`_ - part of OpenStackClient * `Bugs`_ - issue tracking * `Source`_ * `Developer` - getting started as a developer * `Contributing` - contributing code * `Testing` - testing code * IRC: #openstack-sdks on Freenode (irc.freenode.net) * License: Apache 2.0 .. _PyPi: https://pypi.org/project/osc-lib .. _Online Documentation: http://docs.openstack.org/osc-lib/latest/ .. _Launchpad project: https://launchpad.net/python-openstackclient .. _Bugs: https://storyboard.openstack.org/#!/project_group/80 .. _Source: https://opendev.org/openstack/osc-lib .. _Developer: http://docs.openstack.org/project-team-guide/project-setup/python.html .. _Contributing: http://docs.openstack.org/infra/manual/developers.html .. _Testing: http://docs.openstack.org/osc-lib/latest/contributor/#testing .. _Release Notes: https://docs.openstack.org/releasenotes/osc-lib Getting Started =============== osc-lib can be installed from PyPI using pip:: pip install osc-lib Transition From OpenStackclient =============================== This library was extracted from the main OSC repo after the OSC 2.4.0 release. The following are the changes to imports that will cover the majority of transition to using osc-lib: * openstackclient.api.api -> osc_lib.api.api * openstackclient.api.auth -> osc_lib.api.auth * openstackclient.api.utils -> osc_lib.api.utils * openstackclient.common.command -> osc_lib.command.command * openstackclient.common.commandmanager -> osc_lib.command.commandmanager * openstackclient.common.exceptions -> osc_lib.exceptions * openstackclient.common.logs -> osc_lib.logs * openstackclient.common.parseractions -> osc_lib.cli.parseractions * openstackclient.common.session -> osc_lib.session * openstackclient.common.utils -> osc_lib.utils * openstackclient.i18n -> osc_lib.i18n * openstackclient.shell -> osc_lib.shell Also, some of the test fixtures and modules may be used: * openstackclient.tests.fakes -> osc_lib.tests.fakes * openstackclient.tests.utils -> osc_lib.tests.utils Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 osc-lib-2.0.0/.stestr.conf0000664000175000017500000000013013607371235015335 0ustar zuulzuul00000000000000[DEFAULT] test_path=${OS_TEST_PATH:-./osc_lib/tests} top_dir=./ group_regex=([^\.]+\.)+ osc-lib-2.0.0/tox.ini0000664000175000017500000000377713607371235014423 0ustar zuulzuul00000000000000[tox] minversion = 2.0 envlist = py37,pep8 skipdist = True [testenv] basepython = python3 usedevelop = True install_command = pip install {opts} {packages} setenv = VIRTUAL_ENV={envdir} OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} whitelist_externals = stestr [testenv:unit-tips] commands = pip install -q -e "git+file://{toxinidir}/../cliff#egg=cliff" pip install -q -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth" pip install -q -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" pip freeze stestr run {posargs} whitelist_externals = stestr [testenv:pep8] commands = flake8 [testenv:venv] commands = {posargs} [testenv:cover] commands = python setup.py test --coverage --coverage-package-name=osc_lib --testr-args='{posargs}' coverage report [testenv:debug] commands = oslo_debug_helper -t osc_lib/tests {posargs} [testenv:docs] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-apidoc -o doc/api osc_lib osc_lib/tests # Create ChangeLog file python setup.py sdist sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html [testenv:releasenotes] commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] show-source = True exclude = .git,.tox,dist,doc,*lib/python*,*egg,build,tools # If 'ignore' is not set there are default errors and warnings that are set # Doc: http://flake8.readthedocs.org/en/latest/config.html#default ignore = __ [testenv:lower-constraints] deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt osc-lib-2.0.0/AUTHORS0000664000175000017500000001322113607371310014133 0ustar zuulzuul00000000000000Aaron Rosen Abhishek Chanda Akihiro Motoki Akihiro Motoki Alessandro Pilotti Alessio Ababilov Alex Gaynor Alex Schultz Alexander Ignatov Alexander Tsamutali Alvaro Lopez Garcia Amey Bhide Andreas Jaeger Andreas Jaeger Andrey Kurilin Atsushi SAKAI Bar Elharar Bharat Kunwar Bhuvan Arumugam Brad Behle Brandon Palm Cao Xuan Hoang Cedric Brandily Chaozhe.Chen Christian Berendt Chuck Short Clark Boylan Colleen Murphy Corey Bryant Cyril Roelandt Daisuke Fujita Daniel Gonzalez Daniel Speichert Dao Cong Tien Dave Chen David Moreau Simard Dean Troyer Dina Belova Dmitry Tantsur Doug Hellmann Doug Hellmann Dougal Matthews Eric Brown Eric Fried Florent Flament Ghanshyam Mann Guojian Shao Hangdong Zhang Hidekazu Nakamura Hideki Saito Hieu LE Hironori Shiina Huanxuan Ao Hugh Saunders Ian Wienand Igor_Bolotin Ilya Shakhat Jake Yip James E. Blair James E. Blair Jamie Lennox Jamie Lennox Jas Javier Pena Jeremy Stanley Jerry George Jesse Keating Joe Gordon Josh Kearney Joshua Harlow Juan Antonio Osorio Robles Jude Job Julien Lavesque Ken Thomas Kristi Nikolla Kyrylo Romanenko Luong Anh Tuan Marek Aufart Marek Denis Mark Vanderwiel Matt Fischer Matt Joyce Matt Riedemann Matthew Treinish Matthieu Huin Michael Johnson Michael McCune Monty Taylor Mouad Benchchaoui Nam Nguyen Hoai Nathan Kinder Noorul Islam K M Oleksii Chuprykov Ondřej Nový OpenStack Release Bot Paul Belanger Qiu Yu Rabi Mishra Reedip Richard Theis Rodrigo Duarte Sousa Roey Chen Roxana Gherle Rui Chen SaiKiran Sascha Peilicke Sascha Peilicke Sheel Rana Sirushti Murugesan Stephen Finucane Steve Martinelli Steve Martinelli Swapnil Kulkarni (coolsvap) Tang Chen Tang Chen Telles Nobrega Terry Howe TerryHowe Thomas Bechtold Thomas Goirand Tim Burke Tom Cocozzello Tony Breeds Tovin Seven Ukesh Kumar Vasudevan Vladyslav Drok Wenzhi Yu Xi Yang YAMAMOTO Takashi Zhenguo Niu ZhongShengping chengkunye guang-yee heha henriquetruta jiaxi jichenjc kavithahr lin-hua-cheng luqitao pengyuesheng qingszhao qtang rabi reedip sunyajing ting.wang venkatamahesh wanghong wangqi xiaozhuangqing xiexs yanpuqing zhiyuan_cai zhurong osc-lib-2.0.0/osc_lib.egg-info/0000775000175000017500000000000013607371310016170 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib.egg-info/PKG-INFO0000664000175000017500000000677713607371310017306 0ustar zuulzuul00000000000000Metadata-Version: 1.1 Name: osc-lib Version: 2.0.0 Summary: OpenStackClient Library Home-page: https://docs.openstack.org/osc-lib/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ======= osc-lib ======= .. image:: https://img.shields.io/pypi/v/osc-lib.svg :target: https://pypi.org/project/osc-lib/ :alt: Latest Version OpenStackClient (aka OSC) is a command-line client for OpenStack. osc-lib is a package of common support modules for writing OSC plugins. * `PyPi`_ - package installation * `Online Documentation`_ * `Launchpad project`_ - part of OpenStackClient * `Bugs`_ - issue tracking * `Source`_ * `Developer` - getting started as a developer * `Contributing` - contributing code * `Testing` - testing code * IRC: #openstack-sdks on Freenode (irc.freenode.net) * License: Apache 2.0 .. _PyPi: https://pypi.org/project/osc-lib .. _Online Documentation: http://docs.openstack.org/osc-lib/latest/ .. _Launchpad project: https://launchpad.net/python-openstackclient .. _Bugs: https://storyboard.openstack.org/#!/project_group/80 .. _Source: https://opendev.org/openstack/osc-lib .. _Developer: http://docs.openstack.org/project-team-guide/project-setup/python.html .. _Contributing: http://docs.openstack.org/infra/manual/developers.html .. _Testing: http://docs.openstack.org/osc-lib/latest/contributor/#testing .. _Release Notes: https://docs.openstack.org/releasenotes/osc-lib Getting Started =============== osc-lib can be installed from PyPI using pip:: pip install osc-lib Transition From OpenStackclient =============================== This library was extracted from the main OSC repo after the OSC 2.4.0 release. The following are the changes to imports that will cover the majority of transition to using osc-lib: * openstackclient.api.api -> osc_lib.api.api * openstackclient.api.auth -> osc_lib.api.auth * openstackclient.api.utils -> osc_lib.api.utils * openstackclient.common.command -> osc_lib.command.command * openstackclient.common.commandmanager -> osc_lib.command.commandmanager * openstackclient.common.exceptions -> osc_lib.exceptions * openstackclient.common.logs -> osc_lib.logs * openstackclient.common.parseractions -> osc_lib.cli.parseractions * openstackclient.common.session -> osc_lib.session * openstackclient.common.utils -> osc_lib.utils * openstackclient.i18n -> osc_lib.i18n * openstackclient.shell -> osc_lib.shell Also, some of the test fixtures and modules may be used: * openstackclient.tests.fakes -> osc_lib.tests.fakes * openstackclient.tests.utils -> osc_lib.tests.utils Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 osc-lib-2.0.0/osc_lib.egg-info/pbr.json0000664000175000017500000000005613607371310017647 0ustar zuulzuul00000000000000{"git_version": "fea31fb", "is_release": true}osc-lib-2.0.0/osc_lib.egg-info/not-zip-safe0000664000175000017500000000000113607371310020416 0ustar zuulzuul00000000000000 osc-lib-2.0.0/osc_lib.egg-info/dependency_links.txt0000664000175000017500000000000113607371310022236 0ustar zuulzuul00000000000000 osc-lib-2.0.0/osc_lib.egg-info/requires.txt0000664000175000017500000000027413607371310020573 0ustar zuulzuul00000000000000pbr!=2.1.0,>=2.0.0 six>=1.10.0 Babel!=2.4.0,>=2.3.4 cliff!=2.9.0,>=2.8.0 keystoneauth1>=3.7.0 openstacksdk>=0.15.0 oslo.i18n>=3.15.3 oslo.utils>=3.33.0 simplejson>=3.5.1 stevedore>=1.20.0 osc-lib-2.0.0/osc_lib.egg-info/SOURCES.txt0000664000175000017500000000641213607371310020057 0ustar zuulzuul00000000000000.coveragerc .mailmap .stestr.conf .testr.conf .zuul.yaml AUTHORS ChangeLog HACKING.rst LICENSE README.rst lower-constraints.txt requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/Makefile doc/requirements.txt doc/ext/__init__.py doc/ext/apidoc.py doc/source/conf.py doc/source/index.rst doc/source/contributor/index.rst doc/source/reference/index.rst doc/source/user/change_log.rst doc/source/user/index.rst doc/source/user/transition.rst osc_lib/__init__.py osc_lib/clientmanager.py osc_lib/exceptions.py osc_lib/i18n.py osc_lib/logs.py osc_lib/shell.py osc_lib/version.py osc_lib.egg-info/PKG-INFO osc_lib.egg-info/SOURCES.txt osc_lib.egg-info/dependency_links.txt osc_lib.egg-info/not-zip-safe osc_lib.egg-info/pbr.json osc_lib.egg-info/requires.txt osc_lib.egg-info/top_level.txt osc_lib/api/__init__.py osc_lib/api/api.py osc_lib/api/auth.py osc_lib/api/utils.py osc_lib/cli/__init__.py osc_lib/cli/client_config.py osc_lib/cli/format_columns.py osc_lib/cli/identity.py osc_lib/cli/parseractions.py osc_lib/command/__init__.py osc_lib/command/command.py osc_lib/command/commandmanager.py osc_lib/command/timing.py osc_lib/tests/__init__.py osc_lib/tests/fakes.py osc_lib/tests/test_clientmanager.py osc_lib/tests/test_logs.py osc_lib/tests/test_shell.py osc_lib/tests/api/__init__.py osc_lib/tests/api/fakes.py osc_lib/tests/api/test_api.py osc_lib/tests/api/test_utils.py osc_lib/tests/cli/__init__.py osc_lib/tests/cli/test_client_config.py osc_lib/tests/cli/test_format_columns.py osc_lib/tests/cli/test_identity.py osc_lib/tests/cli/test_parseractions.py osc_lib/tests/command/__init__.py osc_lib/tests/command/test_command.py osc_lib/tests/command/test_commandmanager.py osc_lib/tests/command/test_timing.py osc_lib/tests/utils/__init__.py osc_lib/tests/utils/test_columns.py osc_lib/tests/utils/test_tags.py osc_lib/tests/utils/test_utils.py osc_lib/utils/__init__.py osc_lib/utils/columns.py osc_lib/utils/tags.py releasenotes/notes/.placeholder releasenotes/notes/1.0-summary-47dcce446d6a512b.yaml releasenotes/notes/add-KeyValueAppendAction-class-f830e71152d6b91e.yaml releasenotes/notes/add-MultiKeyValueCommaAction-class-01dd254a287d70d2.yaml releasenotes/notes/arg-precedence-1ba9fd6929650830.yaml releasenotes/notes/auth-type-none-d96760912605f822.yaml releasenotes/notes/bug-1558690-1528b637f2c0a449.yaml releasenotes/notes/bug-1630822-mask-password-on-debug-20dcdf1c54e84fa1.yaml releasenotes/notes/bug-2004898-686577a07e791051.yaml releasenotes/notes/bug-2006480-436489d39643e76c.yaml releasenotes/notes/direct-openstacksdk-535a179f3c645cc0.yaml releasenotes/notes/drop-py2-support-60c93244107d5778.yaml releasenotes/notes/find-project-203bf867619c557e.yaml releasenotes/notes/keystone-to-keystone-9b2e55b051775322.yaml releasenotes/notes/none-auth-cli-48ab0e48d4852941.yaml releasenotes/notes/os-profile-as-environment-variable-a5e232e9ca7c5171.yaml releasenotes/notes/shell-argv-decode-cdc13dc0c4ec07af.yaml releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholderosc-lib-2.0.0/osc_lib.egg-info/top_level.txt0000664000175000017500000000001013607371310020711 0ustar zuulzuul00000000000000osc_lib osc-lib-2.0.0/osc_lib/0000775000175000017500000000000013607371310014476 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/command/0000775000175000017500000000000013607371310016114 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/command/commandmanager.py0000664000175000017500000000377213607371235021456 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # 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. # """Modify cliff.CommandManager""" import pkg_resources import cliff.commandmanager class CommandManager(cliff.commandmanager.CommandManager): """Add additional functionality to cliff.CommandManager Load additional command groups after initialization Add _command_group() methods """ def __init__(self, namespace, convert_underscores=True): self.group_list = [] super(CommandManager, self).__init__(namespace, convert_underscores) def load_commands(self, namespace): self.group_list.append(namespace) return super(CommandManager, self).load_commands(namespace) def add_command_group(self, group=None): """Adds another group of command entrypoints""" if group: self.load_commands(group) def get_command_groups(self): """Returns a list of the loaded command groups""" return self.group_list def get_command_names(self, group=None): """Returns a list of commands loaded for the specified group""" group_list = [] if group is not None: for ep in pkg_resources.iter_entry_points(group): cmd_name = ( ep.name.replace('_', ' ') if self.convert_underscores else ep.name ) group_list.append(cmd_name) return group_list return list(self.commands.keys()) osc-lib-2.0.0/osc_lib/command/__init__.py0000664000175000017500000000000013607371235020221 0ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/command/command.py0000664000175000017500000000367713607371235020127 0ustar zuulzuul00000000000000# Copyright 2016 NEC Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import logging from cliff import command from cliff import lister from cliff import show import six from osc_lib import exceptions from osc_lib.i18n import _ class CommandMeta(abc.ABCMeta): def __new__(mcs, name, bases, cls_dict): if 'log' not in cls_dict: cls_dict['log'] = logging.getLogger( cls_dict['__module__'] + '.' + name) return super(CommandMeta, mcs).__new__(mcs, name, bases, cls_dict) @six.add_metaclass(CommandMeta) class Command(command.Command): def run(self, parsed_args): self.log.debug('run(%s)', parsed_args) return super(Command, self).run(parsed_args) def validate_os_beta_command_enabled(self): if not self.app.options.os_beta_command: msg = _('Caution: This is a beta command and subject to ' 'change. Use global option --os-beta-command ' 'to enable this command.') raise exceptions.CommandError(msg) def deprecated_option_warning(self, old_option, new_option): """Emit a warning for use of a deprecated option""" self.log.warning( _("The %(old)s option is deprecated, please use %(new)s instead.") % {'old': old_option, 'new': new_option} ) class Lister(Command, lister.Lister): pass class ShowOne(Command, show.ShowOne): pass osc-lib-2.0.0/osc_lib/command/timing.py0000664000175000017500000000216313607371235017765 0ustar zuulzuul00000000000000# 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. # """Timing Implementation""" from osc_lib.command import command class Timing(command.Lister): """Show timing data""" def take_action(self, parsed_args): column_headers = ( 'URL', 'Seconds', ) results = [] total = 0.0 for td in self.app.timing_data: sec = td.elapsed.total_seconds() total += sec results.append((td.method + ' ' + td.url, sec)) results.append(('Total', total)) return ( column_headers, results, ) osc-lib-2.0.0/osc_lib/i18n.py0000664000175000017500000000141713607371235015640 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import oslo_i18n _translators = oslo_i18n.TranslatorFactory(domain='osc_lib') # The primary translation function using the well-known name "_" _ = _translators.primary osc-lib-2.0.0/osc_lib/utils/0000775000175000017500000000000013607371310015636 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/utils/tags.py0000664000175000017500000001655313607371235017166 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import argparse from osc_lib.i18n import _ class _CommaListAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, self.dest, values.split(',')) def add_tag_filtering_option_to_parser( parser, resource_name, enhance_help=lambda _h: _h): """Add tag filtering options to a parser. :param parser: argparse.Argument parser object. :param resource_name: Description of the object being filtered. :param enhance_help: A callable accepting a single parameter, the (translated) help string, and returning a (translated) help string. May be used by a caller wishing to add qualifying text, such as "Applies to version XYZ only", to the help strings for all options produced by this method. """ parser.add_argument( '--tags', metavar='[,,...]', action=_CommaListAction, help=enhance_help( _('List %s which have all given tag(s) ' '(Comma-separated list of tags)') % resource_name) ) parser.add_argument( '--any-tags', metavar='[,,...]', action=_CommaListAction, help=enhance_help( _('List %s which have any given tag(s) ' '(Comma-separated list of tags)') % resource_name) ) parser.add_argument( '--not-tags', metavar='[,,...]', action=_CommaListAction, help=enhance_help( _('Exclude %s which have all given tag(s) ' '(Comma-separated list of tags)') % resource_name) ) parser.add_argument( '--not-any-tags', metavar='[,,...]', action=_CommaListAction, help=enhance_help( _('Exclude %s which have any given tag(s) ' '(Comma-separated list of tags)') % resource_name) ) def get_tag_filtering_args(parsed_args, args): """Adds the tag arguments to an args list. Intended to be used to append the tags to an argument list that will be used for service client. :param parsed_args: Parsed argument object returned by argparse parse_args. :param args: The argument list to add tags to. """ if parsed_args.tags: args['tags'] = ','.join(parsed_args.tags) if parsed_args.any_tags: args['any_tags'] = ','.join(parsed_args.any_tags) if parsed_args.not_tags: args['not_tags'] = ','.join(parsed_args.not_tags) if parsed_args.not_any_tags: args['not_any_tags'] = ','.join(parsed_args.not_any_tags) def add_tag_option_to_parser_for_create( parser, resource_name, enhance_help=lambda _h: _h): """Add tag options to a parser for create commands. :param parser: argparse.Argument parser object. :param resource_name: Description of the object being filtered. :param enhance_help: A callable accepting a single parameter, the (translated) help string, and returning a (translated) help string. May be used by a caller wishing to add qualifying text, such as "Applies to version XYZ only", to the help strings for all options produced by this method. """ tag_group = parser.add_mutually_exclusive_group() tag_group.add_argument( '--tag', action='append', dest='tags', metavar='', help=enhance_help( _("Tag to be added to the %s " "(repeat option to set multiple tags)") % resource_name) ) tag_group.add_argument( '--no-tag', action='store_true', help=enhance_help(_("No tags associated with the %s") % resource_name) ) def add_tag_option_to_parser_for_set( parser, resource_name, enhance_help=lambda _h: _h): """Add tag options to a parser for set commands. :param parser: argparse.Argument parser object. :param resource_name: Description of the object being filtered. :param enhance_help: A callable accepting a single parameter, the (translated) help string, and returning a (translated) help string. May be used by a caller wishing to add qualifying text, such as "Applies to version XYZ only", to the help strings for all options produced by this method. """ parser.add_argument( '--tag', action='append', dest='tags', metavar='', help=enhance_help( _("Tag to be added to the %s " "(repeat option to set multiple tags)") % resource_name) ) parser.add_argument( '--no-tag', action='store_true', help=enhance_help( _("Clear tags associated with the %s. Specify both " "--tag and --no-tag to overwrite current tags") % resource_name) ) def add_tag_option_to_parser_for_unset( parser, resource_name, enhance_help=lambda _h: _h): """Add tag options to a parser for set commands. :param parser: argparse.Argument parser object. :param resource_name: Description of the object being filtered. :param enhance_help: A callable accepting a single parameter, the (translated) help string, and returning a (translated) help string. May be used by a caller wishing to add qualifying text, such as "Applies to version XYZ only", to the help strings for all options produced by this method. """ tag_group = parser.add_mutually_exclusive_group() tag_group.add_argument( '--tag', action='append', dest='tags', metavar='', help=enhance_help( _("Tag to be removed from the %s " "(repeat option to remove multiple tags)") % resource_name)) tag_group.add_argument( '--all-tag', action='store_true', help=enhance_help( _("Clear all tags associated with the %s") % resource_name)) def update_tags_for_set(client, obj, parsed_args): """Set the tags on an object. :param client: The service client to use setting the tags. :param obj: The object (Resource) to set the tags on. :param parsed_args: Parsed argument object returned by argparse parse_args. """ if parsed_args.no_tag: tags = set() else: tags = set(obj.tags or []) if parsed_args.tags: tags |= set(parsed_args.tags) if set(obj.tags or []) != tags: client.set_tags(obj, sorted(list(tags))) def update_tags_for_unset(client, obj, parsed_args): """Unset the tags on an object. :param client: The service client to use unsetting the tags. :param obj: The object (Resource) to unset the tags on. :param parsed_args: Parsed argument object returned by argparse parse_args. """ tags = set(obj.tags) if parsed_args.all_tag: tags = set() if parsed_args.tags: tags -= set(parsed_args.tags) if set(obj.tags) != tags: client.set_tags(obj, sorted(list(tags))) osc-lib-2.0.0/osc_lib/utils/__init__.py0000664000175000017500000006212313607371235017761 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # 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. # """Common client utilities""" import copy import getpass import logging import os import six import time import warnings from cliff import columns as cliff_columns from oslo_utils import importutils from osc_lib import exceptions from osc_lib.i18n import _ LOG = logging.getLogger(__name__) def backward_compat_col_lister(column_headers, columns, column_map): """Convert the column headers to keep column backward compatibility. Replace the new column name of column headers by old name, so that the column headers can continue to support to show the old column name by --column/-c option with old name, like: volume list -c 'Display Name' :param column_headers: The column headers to be output in list command. :param columns: The columns to be output. :param column_map: The key of map is old column name, the value is new column name, like: {'old_col': 'new_col'} """ if not columns: return column_headers # NOTE(RuiChen): column_headers may be a tuple in some code, like: # volume v1, convert it to a list in order to change # the column name. column_headers = list(column_headers) for old_col, new_col in six.iteritems(column_map): if old_col in columns: LOG.warning(_('The column "%(old_column)s" was deprecated, ' 'please use "%(new_column)s" replace.') % { 'old_column': old_col, 'new_column': new_col} ) if new_col in column_headers: column_headers[column_headers.index(new_col)] = old_col return column_headers def backward_compat_col_showone(show_object, columns, column_map): """Convert the output object to keep column backward compatibility. Replace the new column name of output object by old name, so that the object can continue to support to show the old column name by --column/-c option with old name, like: volume show -c 'display_name' :param show_object: The object to be output in create/show commands. :param columns: The columns to be output. :param column_map: The key of map is old column name, the value is new column name, like: {'old_col': 'new_col'} """ if not columns: return show_object show_object = copy.deepcopy(show_object) for old_col, new_col in six.iteritems(column_map): if old_col in columns: LOG.warning(_('The column "%(old_column)s" was deprecated, ' 'please use "%(new_column)s" replace.') % { 'old_column': old_col, 'new_column': new_col} ) if new_col in show_object: show_object.update({old_col: show_object.pop(new_col)}) return show_object def build_kwargs_dict(arg_name, value): """Return a dictionary containing `arg_name` if `value` is set.""" kwargs = {} if value: kwargs[arg_name] = value return kwargs def calculate_header_and_attrs(column_headers, attrs, parsed_args): """Calculate headers and attribute names based on parsed_args.column. When --column (-c) option is specified, this function calculates column headers and expected API attribute names according to the OSC header/column definitions. This function also adjusts the content of parsed_args.columns if API attribute names are used in parsed_args.columns. This allows users to specify API attribute names in -c option. :param column_headers: A tuple/list of column headers to display :param attrs: a tuple/list of API attribute names. The order of corresponding column header and API attribute name must match. :param parsed_args: Parsed argument object returned by argparse parse_args :returns: A tuple of calculated headers and API attribute names. """ if parsed_args.columns: header_attr_map = dict(zip(column_headers, attrs)) expected_attrs = [header_attr_map.get(c, c) for c in parsed_args.columns] attr_header_map = dict(zip(attrs, column_headers)) expected_headers = [attr_header_map.get(c, c) for c in parsed_args.columns] # If attribute name is used in parsed_args.columns # convert it into display names because cliff expects # name in parsed_args.columns and name in column_headers matches. parsed_args.columns = expected_headers return expected_headers, expected_attrs else: return column_headers, attrs def env(*vars, **kwargs): """Search for the first defined of possibly many env vars Returns the first environment variable defined in vars, or returns the default defined in kwargs. """ for v in vars: value = os.environ.get(v, None) if value: return value return kwargs.get('default', '') def find_min_match(items, sort_attr, **kwargs): """Find all resources meeting the given minimum constraints :param items: A List of objects to consider :param sort_attr: Attribute to sort the resulting list :param kwargs: A dict of attributes and their minimum values :rtype: A list of resources osrted by sort_attr that meet the minimums """ def minimum_pieces_of_flair(item): """Find lowest value greater than the minumum""" result = True for k in kwargs: # AND together all of the given attribute results result = result and kwargs[k] <= get_field(item, k) return result return sort_items(filter(minimum_pieces_of_flair, items), sort_attr) def find_resource(manager, name_or_id, **kwargs): """Helper for the _find_* methods. :param manager: A client manager class :param name_or_id: The resource we are trying to find :param kwargs: To be used in calling .find() :rtype: The found resource This method will attempt to find a resource in a variety of ways. Primarily .get() methods will be called with `name_or_id` as an integer value, and tried again as a string value. If both fail, then a .find() is attempted, which is essentially calling a .list() function with a 'name' query parameter that is set to `name_or_id`. Lastly, if any kwargs are passed in, they will be treated as additional query parameters. This is particularly handy in the case of finding resources in a domain. """ # Case 1: name_or_id is an ID, we need to call get() directly # for example: /projects/454ad1c743e24edcad846d1118837cac # For some projects, the name only will work. For keystone, this is not # enough information, and domain information is necessary. try: return manager.get(name_or_id) except Exception: pass if kwargs: # Case 2: name_or_id is a name, but we have query args in kwargs # for example: /projects/demo&domain_id=30524568d64447fbb3fa8b7891c10dd try: return manager.get(name_or_id, **kwargs) except Exception: pass # Case 3: Try to get entity as integer id. Keystone does not have integer # IDs, they are UUIDs, but some things in nova do, like flavors. try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id), **kwargs) # FIXME(dtroyer): The exception to catch here is dependent on which # client library the manager passed in belongs to. # Eventually this should be pulled from a common set # of client exceptions. except Exception as ex: if (type(ex).__name__ == 'NotFound' or type(ex).__name__ == 'HTTPNotFound' or type(ex).__name__ == 'TypeError'): pass else: raise # Case 4: Try to use find. # Reset the kwargs here for find if len(kwargs) == 0: kwargs = {} try: # Prepare the kwargs for calling find if 'NAME_ATTR' in manager.resource_class.__dict__: # novaclient does this for oddball resources kwargs[manager.resource_class.NAME_ATTR] = name_or_id else: kwargs['name'] = name_or_id except Exception: pass # finally try to find entity by name try: return manager.find(**kwargs) # FIXME(dtroyer): The exception to catch here is dependent on which # client library the manager passed in belongs to. # Eventually this should be pulled from a common set # of client exceptions. except Exception as ex: if type(ex).__name__ == 'NotFound': msg = _( "No %(resource)s with a name or ID of '%(id)s' exists." ) raise exceptions.CommandError(msg % { 'resource': manager.resource_class.__name__.lower(), 'id': name_or_id, }) if type(ex).__name__ == 'NoUniqueMatch': msg = _( "More than one %(resource)s exists with the name '%(id)s'." ) raise exceptions.CommandError(msg % { 'resource': manager.resource_class.__name__.lower(), 'id': name_or_id, }) if type(ex).__name__ == 'Forbidden': msg = _( "You are not authorized to find %(resource)s with the " "name '%(id)s'." ) raise exceptions.CommandError(msg % { 'resource': manager.resource_class.__name__.lower(), 'id': name_or_id, }) else: pass # Case 5: For client with no find function, list all resources and hope # to find a matching name or ID. count = 0 for resource in manager.list(): if (resource.get('id') == name_or_id or resource.get('name') == name_or_id): count += 1 _resource = resource if count == 0: # we found no match, report back this error: msg = _("Could not find resource %s") raise exceptions.CommandError(msg % name_or_id) elif count == 1: return _resource else: # we found multiple matches, report back this error msg = _("More than one resource exists with the name or ID '%s'.") raise exceptions.CommandError(msg % name_or_id) def format_dict(data, prefix=None): """Return a formatted string of key value pairs :param data: a dict :param prefix: the current parent keys in a recursive call :rtype: a string formatted to key='value' """ if data is None: return None output = "" for s in sorted(data): if prefix: key_str = ".".join([prefix, s]) else: key_str = s if isinstance(data[s], dict): # NOTE(dtroyer): Only append the separator chars here, quoting # is completely handled in the terminal case. output = output + format_dict(data[s], prefix=key_str) + ", " elif data[s] is not None: output = output + key_str + "='" + six.text_type(data[s]) + "', " else: output = output + key_str + "=, " return output[:-2] def format_dict_of_list(data, separator='; '): """Return a formatted string of key value pair :param data: a dict, key is string, value is a list of string, for example: {u'public': [u'2001:db8::8', u'172.24.4.6']} :param separator: the separator to use between key/value pair (default: '; ') :return: a string formatted to {'key1'=['value1', 'value2']} with separated by separator """ if data is None: return None output = [] for key in sorted(data): value = data[key] if value is None: continue value_str = format_list(value) group = "%s=%s" % (key, value_str) output.append(group) return separator.join(output) def format_list(data, separator=', '): """Return a formatted strings :param data: a list of strings :param separator: the separator to use between strings (default: ', ') :rtype: a string formatted based on separator """ if data is None: return None return separator.join(sorted(data)) def format_list_of_dicts(data): """Return a formatted string of key value pairs for each dict :param data: a list of dicts :rtype: a string formatted to key='value' with dicts separated by new line """ if data is None: return None return '\n'.join(format_dict(i) for i in data) def format_size(size): """Display size of a resource in a human readable format :param string size: The size of the resource in bytes. :returns: Returns the size in human-friendly format :rtype string: This function converts the size (provided in bytes) of a resource into a human-friendly format such as K, M, G, T, P, E, Z """ suffix = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z'] base = 1000.0 index = 0 if size is None: size = 0 while size >= base: index = index + 1 size = size / base padded = '%.1f' % size stripped = padded.rstrip('0').rstrip('.') return '%s%s' % (stripped, suffix[index]) def get_client_class(api_name, version, version_map): """Returns the client class for the requested API version :param api_name: the name of the API, e.g. 'compute', 'image', etc :param version: the requested API version :param version_map: a dict of client classes keyed by version :rtype: a client class for the requested API version """ try: client_path = version_map[str(version)] except (KeyError, ValueError): sorted_versions = sorted(version_map.keys(), key=lambda s: list(map(int, s.split('.')))) msg = _( "Invalid %(api_name)s client version '%(version)s'. " "must be one of: %(version_map)s" ) raise exceptions.UnsupportedVersion(msg % { 'api_name': api_name, 'version': version, 'version_map': ', '.join(sorted_versions), }) return importutils.import_class(client_path) def get_dict_properties(item, fields, mixed_case_fields=None, formatters=None): """Return a tuple containing the item properties. :param item: a single dict resource :param fields: tuple of strings with the desired field names :param mixed_case_fields: tuple of field names to preserve case :param formatters: dictionary mapping field names to callables to format the values """ if mixed_case_fields is None: mixed_case_fields = [] if formatters is None: formatters = {} row = [] for field in fields: if field in mixed_case_fields: field_name = field.replace(' ', '_') else: field_name = field.lower().replace(' ', '_') data = item[field_name] if field_name in item else '' if field in formatters: formatter = formatters[field] if (isinstance(formatter, type) and issubclass( formatter, cliff_columns.FormattableColumn)): data = formatter(data) elif callable(formatter): warnings.warn( 'The usage of formatter functions is now discouraged. ' 'Consider using cliff.columns.FormattableColumn instead. ' 'See reviews linked with bug 1687955 for more detail.', category=DeprecationWarning) if data is not None: data = formatter(data) else: msg = "Invalid formatter provided." raise exceptions.CommandError(msg) row.append(data) return tuple(row) def get_effective_log_level(): """Returns the lowest logging level considered by logging handlers Retrieve and return the smallest log level set among the root logger's handlers (in case of multiple handlers). """ root_log = logging.getLogger() min_log_lvl = logging.CRITICAL for handler in root_log.handlers: min_log_lvl = min(min_log_lvl, handler.level) return min_log_lvl def get_field(item, field): try: if isinstance(item, dict): return item[field] else: return getattr(item, field) except Exception: msg = _("Resource doesn't have field %s") raise exceptions.CommandError(msg % field) def get_item_properties(item, fields, mixed_case_fields=None, formatters=None): """Return a tuple containing the item properties. :param item: a single item resource (e.g. Server, Project, etc) :param fields: tuple of strings with the desired field names :param mixed_case_fields: tuple of field names to preserve case :param formatters: dictionary mapping field names to callables to format the values """ if mixed_case_fields is None: mixed_case_fields = [] if formatters is None: formatters = {} row = [] for field in fields: if field in mixed_case_fields: field_name = field.replace(' ', '_') else: field_name = field.lower().replace(' ', '_') data = getattr(item, field_name, '') if field in formatters: formatter = formatters[field] if (isinstance(formatter, type) and issubclass( formatter, cliff_columns.FormattableColumn)): data = formatter(data) elif callable(formatter): warnings.warn( 'The usage of formatter functions is now discouraged. ' 'Consider using cliff.columns.FormattableColumn instead. ' 'See reviews linked with bug 1687955 for more detail.', category=DeprecationWarning) if data is not None: data = formatter(data) else: msg = "Invalid formatter provided." raise exceptions.CommandError(msg) row.append(data) return tuple(row) def get_password(stdin, prompt=None, confirm=True): message = prompt or "User Password:" if hasattr(stdin, 'isatty') and stdin.isatty(): try: while True: first_pass = getpass.getpass(message) if not confirm: return first_pass second_pass = getpass.getpass("Repeat " + message) if first_pass == second_pass: return first_pass msg = _("The passwords entered were not the same") print(msg) except EOFError: # Ctl-D msg = _("Error reading password") raise exceptions.CommandError(msg) msg = _("No terminal detected attempting to read password") raise exceptions.CommandError(msg) def is_ascii(string): try: (string.decode('ascii') if isinstance(string, bytes) else string.encode('ascii')) return True except (UnicodeEncodeError, UnicodeDecodeError): return False def read_blob_file_contents(blob_file): try: with open(blob_file) as file: blob = file.read().strip() return blob except IOError: msg = _("Error occurred trying to read from file %s") raise exceptions.CommandError(msg % blob_file) def sort_items(items, sort_str, sort_type=None): """Sort items based on sort keys and sort directions given by sort_str. :param items: a list or generator object of items :param sort_str: a string defining the sort rules, the format is ':[direction1],:[direction2]...', direction can be 'asc' for ascending or 'desc' for descending, if direction is not given, it's ascending by default :return: sorted items """ if not sort_str: return items # items may be a generator object, transform it to a list items = list(items) sort_keys = sort_str.strip().split(',') for sort_key in reversed(sort_keys): reverse = False if ':' in sort_key: sort_key, direction = sort_key.split(':', 1) if not sort_key: msg = _("''' is not a valid sort key") raise exceptions.CommandError(msg) if direction not in ['asc', 'desc']: if not direction: direction = "" msg = _( "'%(direction)s' is not a valid sort direction for " "sort key %(sort_key)s, use 'asc' or 'desc' instead" ) raise exceptions.CommandError(msg % { 'direction': direction, 'sort_key': sort_key, }) if direction == 'desc': reverse = True def f(x): # Attempts to convert items to same 'sort_type' if provided. # This is due to Python 3 throwing TypeError if you attempt to # compare different types item = get_field(x, sort_key) if sort_type and not isinstance(item, sort_type): try: item = sort_type(item) except Exception: # Can't convert, so no sensible way to compare item = sort_type() return item items.sort(key=f, reverse=reverse) return items def wait_for_delete(manager, res_id, status_field='status', error_status=['error'], exception_name=['NotFound'], sleep_time=5, timeout=300, callback=None): """Wait for resource deletion :param manager: the manager from which we can get the resource :param res_id: the resource id to watch :param status_field: the status attribute in the returned resource object, this is used to check for error states while the resource is being deleted :param error_status: a list of status strings for error :param exception_name: a list of exception strings for deleted case :param sleep_time: wait this long between checks (seconds) :param timeout: check until this long (seconds) :param callback: called per sleep cycle, useful to display progress; this function is passed a progress value during each iteration of the wait loop :rtype: True on success, False if the resource has gone to error state or the timeout has been reached """ total_time = 0 while total_time < timeout: try: # might not be a bad idea to re-use find_resource here if it was # a bit more friendly in the exceptions it raised so we could just # handle a NotFound exception here without parsing the message res = manager.get(res_id) except Exception as ex: if type(ex).__name__ in exception_name: return True raise status = getattr(res, status_field, '').lower() if status in error_status: return False if callback: progress = getattr(res, 'progress', None) or 0 callback(progress) time.sleep(sleep_time) total_time += sleep_time # if we got this far we've timed out return False def wait_for_status(status_f, res_id, status_field='status', success_status=['active'], error_status=['error'], sleep_time=5, callback=None): """Wait for status change on a resource during a long-running operation :param status_f: a status function that takes a single id argument :param res_id: the resource id to watch :param status_field: the status attribute in the returned resource object :param success_status: a list of status strings for successful completion :param error_status: a list of status strings for error :param sleep_time: wait this long (seconds) :param callback: called per sleep cycle, useful to display progress :rtype: True on success """ while True: res = status_f(res_id) status = getattr(res, status_field, '').lower() if status in success_status: retval = True break elif status in error_status: retval = False break if callback: progress = getattr(res, 'progress', None) or 0 callback(progress) time.sleep(sleep_time) return retval osc-lib-2.0.0/osc_lib/utils/columns.py0000664000175000017500000000740413607371235017703 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import operator LIST_BOTH = 'both' LIST_SHORT_ONLY = 'short_only' LIST_LONG_ONLY = 'long_only' def get_column_definitions(attr_map, long_listing): """Return table headers and column names for a listing table. An attribute map (attr_map) is a list of table entry definitions and the format of the map is as follows: :param attr_map: a list of table entry definitions. Each entry should be a tuple consisting of (API attribute name, header name, listing mode). For example: .. code-block:: python (('id', 'ID', LIST_BOTH), ('name', 'Name', LIST_BOTH), ('tenant_id', 'Project', LIST_LONG_ONLY)) The third field of each tuple must be one of LIST_BOTH, LIST_LONG_ONLY (a corresponding column is shown only in a long mode), or LIST_SHORT_ONLY (a corresponding column is shown only in a short mode). :param long_listing: A boolean value which indicates a long listing or not. In most cases, parsed_args.long is passed to this argument. :return: A tuple of a list of table headers and a list of column names. """ if long_listing: headers = [hdr for col, hdr, listing_mode in attr_map if listing_mode in (LIST_BOTH, LIST_LONG_ONLY)] columns = [col for col, hdr, listing_mode in attr_map if listing_mode in (LIST_BOTH, LIST_LONG_ONLY)] else: headers = [hdr for col, hdr, listing_mode in attr_map if listing_mode if listing_mode in (LIST_BOTH, LIST_SHORT_ONLY)] columns = [col for col, hdr, listing_mode in attr_map if listing_mode if listing_mode in (LIST_BOTH, LIST_SHORT_ONLY)] return headers, columns def get_columns(item, attr_map=None): """Return pair of resource attributes and corresponding display names. :param item: a dictionary which represents a resource. Keys of the dictionary are expected to be attributes of the resource. Values are not referred to by this method. .. code-block:: python {'id': 'myid', 'name': 'myname', 'foo': 'bar', 'tenant_id': 'mytenan'} :param attr_map: a list of mapping from attribute to display name. The same format is used as for get_column_definitions attr_map. .. code-block:: python (('id', 'ID', LIST_BOTH), ('name', 'Name', LIST_BOTH), ('tenant_id', 'Project', LIST_LONG_ONLY)) :return: A pair of tuple of attributes and tuple of display names. .. code-block:: python (('id', 'name', 'tenant_id', 'foo'), # attributes ('ID', 'Name', 'Project', 'foo') # display names Both tuples of attributes and display names are sorted by display names in the alphabetical order. Attributes not found in a given attr_map are kept as-is. """ attr_map = attr_map or tuple([]) _attr_map_dict = dict((col, hdr) for col, hdr, listing_mode in attr_map) columns = [(column, _attr_map_dict.get(column, column)) for column in item.keys()] columns = sorted(columns, key=operator.itemgetter(1)) return (tuple(col[0] for col in columns), tuple(col[1] for col in columns)) osc-lib-2.0.0/osc_lib/cli/0000775000175000017500000000000013607371310015245 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/cli/client_config.py0000664000175000017500000002227113607371235020434 0ustar zuulzuul00000000000000# 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. # """OpenStackConfig subclass for argument compatibility""" import logging from openstack.config import exceptions as sdk_exceptions from openstack.config import loader as config from oslo_utils import strutils import six LOG = logging.getLogger(__name__) # Sublcass OpenStackConfig in order to munge config values # before auth plugins are loaded class OSC_Config(config.OpenStackConfig): def _auth_select_default_plugin(self, config): """Select a default plugin based on supplied arguments Migrated from auth.select_auth_plugin() """ identity_version = str(config.get('identity_api_version', '')) if config.get('username', None) and not config.get('auth_type', None): if identity_version == '3': config['auth_type'] = 'v3password' elif identity_version.startswith('2'): config['auth_type'] = 'v2password' else: # let keystoneauth figure it out itself config['auth_type'] = 'password' elif config.get('token', None) and not config.get('auth_type', None): if identity_version == '3': config['auth_type'] = 'v3token' elif identity_version.startswith('2'): config['auth_type'] = 'v2token' else: # let keystoneauth figure it out itself config['auth_type'] = 'token' else: # The ultimate default is similar to the original behaviour, # but this time with version discovery if not config.get('auth_type', None): config['auth_type'] = 'password' LOG.debug("Auth plugin %s selected" % config['auth_type']) return config def _auth_v2_arguments(self, config): """Set up v2-required arguments from v3 info Migrated from auth.build_auth_params() """ if ('auth_type' in config and config['auth_type'].startswith("v2")): if 'project_id' in config['auth']: config['auth']['tenant_id'] = config['auth']['project_id'] if 'project_name' in config['auth']: config['auth']['tenant_name'] = config['auth']['project_name'] return config def _auth_v2_ignore_v3(self, config): """Remove v3 arguemnts if present for v2 plugin Migrated from clientmanager.setup_auth() """ # NOTE(hieulq): If USER_DOMAIN_NAME, USER_DOMAIN_ID, PROJECT_DOMAIN_ID # or PROJECT_DOMAIN_NAME is present and API_VERSION is 2.0, then # ignore all domain related configs. if (str(config.get('identity_api_version', '')).startswith('2') and config.get('auth_type').endswith('password')): domain_props = [ 'project_domain_id', 'project_domain_name', 'user_domain_id', 'user_domain_name', ] for prop in domain_props: if config['auth'].pop(prop, None) is not None: if config.get('cloud'): LOG.warning( "Ignoring domain related config %s for %s" "because identity API version is 2.0" % ( prop, config['cloud'])) else: LOG.warning( "Ignoring domain related config %s because" " identity API version is 2.0" % prop) return config def _auth_default_domain(self, config): """Set a default domain from available arguments Migrated from clientmanager.setup_auth() """ identity_version = str(config.get('identity_api_version', '')) auth_type = config.get('auth_type', None) # TODO(mordred): This is a usability improvement that's broadly useful # We should port it back up into os-client-config. default_domain = config.get('default_domain', None) if (identity_version == '3' and not auth_type.startswith('v2') and default_domain): # NOTE(stevemar): If PROJECT_DOMAIN_ID or PROJECT_DOMAIN_NAME is # present, then do not change the behaviour. Otherwise, set the # PROJECT_DOMAIN_ID to 'OS_DEFAULT_DOMAIN' for better usability. if ( auth_type in ("password", "v3password", "v3totp") and not config['auth'].get('project_domain_id') and not config['auth'].get('project_domain_name') ): config['auth']['project_domain_id'] = default_domain # NOTE(stevemar): If USER_DOMAIN_ID or USER_DOMAIN_NAME is present, # then do not change the behaviour. Otherwise, set the # USER_DOMAIN_ID to 'OS_DEFAULT_DOMAIN' for better usability. # NOTE(aloga): this should only be set if there is a username. # TODO(dtroyer): Move this to os-client-config after the plugin has # been loaded so we can check directly if the options are accepted. if ( auth_type in ("password", "v3password", "v3totp") and not config['auth'].get('user_domain_id') and not config['auth'].get('user_domain_name') ): config['auth']['user_domain_id'] = default_domain return config def auth_config_hook(self, config): """Allow examination of config values before loading auth plugin OpenStackClient will override this to perform additional chacks on auth_type. """ config = self._auth_select_default_plugin(config) config = self._auth_v2_arguments(config) config = self._auth_v2_ignore_v3(config) config = self._auth_default_domain(config) if LOG.isEnabledFor(logging.DEBUG): LOG.debug("auth_config_hook(): %s", strutils.mask_password(six.text_type(config))) return config def _validate_auth(self, config, loader, fixed_argparse=None): """Validate auth plugin arguments""" # May throw a keystoneauth1.exceptions.NoMatchingPlugin plugin_options = loader.get_options() msgs = [] prompt_options = [] for p_opt in plugin_options: # if it's in config, win, move it and kill it from config dict # if it's in config.auth but not in config we're good # deprecated loses to current # provided beats default, deprecated or not winning_value = self._find_winning_auth_value(p_opt, config) if not winning_value: winning_value = self._find_winning_auth_value( p_opt, config['auth']) # if the plugin tells us that this value is required # then error if it's doesn't exist now if not winning_value and p_opt.required: msgs.append( 'Missing value {auth_key}' ' required for auth plugin {plugin}'.format( auth_key=p_opt.name, plugin=config.get('auth_type'), ) ) # Clean up after ourselves for opt in [p_opt.name] + [o.name for o in p_opt.deprecated]: opt = opt.replace('-', '_') config.pop(opt, None) config['auth'].pop(opt, None) if winning_value: # Prefer the plugin configuration dest value if the value's key # is marked as depreciated. if p_opt.dest is None: config['auth'][p_opt.name.replace('-', '_')] = ( winning_value) else: config['auth'][p_opt.dest] = winning_value # See if this needs a prompting if ( 'prompt' in vars(p_opt) and p_opt.prompt is not None and p_opt.dest not in config['auth'] and self._pw_callback is not None ): # Defer these until we know all required opts are present prompt_options.append(p_opt) if msgs: raise sdk_exceptions.OpenStackConfigException('\n'.join(msgs)) else: for p_opt in prompt_options: config['auth'][p_opt.dest] = self._pw_callback(p_opt.prompt) return config def load_auth_plugin(self, config): """Get auth plugin and validate args""" loader = self._get_auth_loader(config) config = self._validate_auth(config, loader) auth_plugin = loader.load_from_options(**config['auth']) return auth_plugin osc-lib-2.0.0/osc_lib/cli/format_columns.py0000664000175000017500000000263713607371235020665 0ustar zuulzuul00000000000000# Copyright 2017 Huawei, Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # """Formattable column for specify content type""" from cliff import columns from osc_lib import utils class DictColumn(columns.FormattableColumn): """Format column for dict content""" def human_readable(self): return utils.format_dict(self._value) class DictListColumn(columns.FormattableColumn): """Format column for dict, key is string, value is list""" def human_readable(self): return utils.format_dict_of_list(self._value) class ListColumn(columns.FormattableColumn): """Format column for list content""" def human_readable(self): return utils.format_list(self._value) class ListDictColumn(columns.FormattableColumn): """Format column for list of dict content""" def human_readable(self): return utils.format_list_of_dicts(self._value) osc-lib-2.0.0/osc_lib/cli/parseractions.py0000664000175000017500000002574013607371235020512 0ustar zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # # 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. # """argparse Custom Actions""" import argparse from osc_lib.i18n import _ class KeyValueAction(argparse.Action): """A custom action to parse arguments as key=value pairs Ensures that ``dest`` is a dict and values are strings. """ def __call__(self, parser, namespace, values, option_string=None): # Make sure we have an empty dict rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, {}) # Add value if an assignment else remove it if '=' in values: values_list = values.split('=', 1) # NOTE(qtang): Prevent null key setting in property if '' == values_list[0]: msg = _("Property key must be specified: %s") raise argparse.ArgumentTypeError(msg % str(values)) else: getattr(namespace, self.dest, {}).update([values_list]) else: msg = _("Expected 'key=value' type, but got: %s") raise argparse.ArgumentTypeError(msg % str(values)) class KeyValueAppendAction(argparse.Action): """A custom action to parse arguments as key=value pairs Ensures that ``dest`` is a dict and values are lists of strings. """ def __call__(self, parser, namespace, values, option_string=None): # Make sure we have an empty dict rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, {}) # Add value if an assignment else remove it if '=' in values: key, value = values.split('=', 1) # NOTE(qtang): Prevent null key setting in property if '' == key: msg = _("Property key must be specified: %s") raise argparse.ArgumentTypeError(msg % str(values)) dest = getattr(namespace, self.dest) if key in dest: dest[key].append(value) else: dest[key] = [value] else: msg = _("Expected 'key=value' type, but got: %s") raise argparse.ArgumentTypeError(msg % str(values)) class MultiKeyValueAction(argparse.Action): """A custom action to parse arguments as key1=value1,key2=value2 pairs Ensure that ``dest`` is a list. The list will finally contain multiple dicts, with key=value pairs in them. NOTE: The arguments string should be a comma separated key-value pairs. And comma(',') and equal('=') may not be used in the key or value. """ def __init__(self, option_strings, dest, nargs=None, required_keys=None, optional_keys=None, **kwargs): """Initialize the action object, and parse customized options Required keys and optional keys can be specified when initializing the action to enable the key validation. If none of them specified, the key validation will be skipped. :param required_keys: a list of required keys :param optional_keys: a list of optional keys """ if nargs: msg = _("Parameter 'nargs' is not allowed, but got %s") raise ValueError(msg % nargs) super(MultiKeyValueAction, self).__init__(option_strings, dest, **kwargs) # required_keys: A list of keys that is required. None by default. if required_keys and not isinstance(required_keys, list): msg = _("'required_keys' must be a list") raise TypeError(msg) self.required_keys = set(required_keys or []) # optional_keys: A list of keys that is optional. None by default. if optional_keys and not isinstance(optional_keys, list): msg = _("'optional_keys' must be a list") raise TypeError(msg) self.optional_keys = set(optional_keys or []) def __call__(self, parser, namespace, values, metavar=None): # Make sure we have an empty list rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) params = {} for kv in values.split(','): # Add value if an assignment else raise ArgumentTypeError if '=' in kv: kv_list = kv.split('=', 1) # NOTE(qtang): Prevent null key setting in property if '' == kv_list[0]: msg = _("Each property key must be specified: %s") raise argparse.ArgumentTypeError(msg % str(kv)) else: params.update([kv_list]) else: msg = _( "Expected comma separated 'key=value' pairs, but got: %s" ) raise argparse.ArgumentTypeError(msg % str(kv)) # Check key validation valid_keys = self.required_keys | self.optional_keys if valid_keys: invalid_keys = [k for k in params if k not in valid_keys] if invalid_keys: msg = _( "Invalid keys %(invalid_keys)s specified.\n" "Valid keys are: %(valid_keys)s" ) raise argparse.ArgumentTypeError(msg % { 'invalid_keys': ', '.join(invalid_keys), 'valid_keys': ', '.join(valid_keys), }) if self.required_keys: missing_keys = [k for k in self.required_keys if k not in params] if missing_keys: msg = _( "Missing required keys %(missing_keys)s.\n" "Required keys are: %(required_keys)s" ) raise argparse.ArgumentTypeError(msg % { 'missing_keys': ', '.join(missing_keys), 'required_keys': ', '.join(self.required_keys), }) # Update the dest dict getattr(namespace, self.dest, []).append(params) class MultiKeyValueCommaAction(MultiKeyValueAction): """Custom action to parse arguments from a set of key=value pair Ensures that ``dest`` is a dict. Parses dict by separating comma separated string into individual values Ex. key1=val1,val2,key2=val3 => {"key1": "val1,val2", "key2": "val3"} """ def __call__(self, parser, namespace, values, option_string=None): """Overwrite the __call__ function of MultiKeyValueAction This is done to handle scenarios where we may have comma seperated data as a single value. """ # Make sure we have an empty list rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) params = {} key = '' for kv in values.split(','): # Add value if an assignment else raise ArgumentTypeError if '=' in kv: kv_list = kv.split('=', 1) # NOTE(qtang): Prevent null key setting in property if '' == kv_list[0]: msg = _("A key must be specified before '=': %s") raise argparse.ArgumentTypeError(msg % str(kv)) else: params.update([kv_list]) key = kv_list[0] else: # If the ',' split does not have key=value pair, then it # means the current value is a part of the previous # key=value pair, so append it. try: params[key] = "%s,%s" % (params[key], kv) except KeyError: msg = _("A key=value pair is required: %s") raise argparse.ArgumentTypeError(msg % str(kv)) # Check key validation valid_keys = self.required_keys | self.optional_keys if valid_keys: invalid_keys = [k for k in params if k not in valid_keys] if invalid_keys: msg = _( "Invalid keys %(invalid_keys)s specified.\n" "Valid keys are: %(valid_keys)s" ) raise argparse.ArgumentTypeError(msg % { 'invalid_keys': ', '.join(invalid_keys), 'valid_keys': ', '.join(valid_keys), }) if self.required_keys: missing_keys = [k for k in self.required_keys if k not in params] if missing_keys: msg = _( "Missing required keys %(missing_keys)s.\n" "Required keys are: %(required_keys)s" ) raise argparse.ArgumentTypeError(msg % { 'missing_keys': ', '.join(missing_keys), 'required_keys': ', '.join(self.required_keys), }) # Update the dest dict getattr(namespace, self.dest, []).append(params) class RangeAction(argparse.Action): """A custom action to parse a single value or a range of values Parses single integer values or a range of integer values delimited by a colon and returns a tuple of integers: '4' sets ``dest`` to (4, 4) '6:9' sets ``dest`` to (6, 9) """ def __call__(self, parser, namespace, values, option_string=None): range = values.split(':') if len(range) == 0: # Nothing passed, return a zero default setattr(namespace, self.dest, (0, 0)) elif len(range) == 1: # Only a single value is present setattr(namespace, self.dest, (int(range[0]), int(range[0]))) elif len(range) == 2: # Range of two values if int(range[0]) <= int(range[1]): setattr(namespace, self.dest, (int(range[0]), int(range[1]))) else: msg = _("Invalid range, %(min)s is not less than %(max)s") raise argparse.ArgumentError(self, msg % { 'min': range[0], 'max': range[1], }) else: # Too many values msg = _("Invalid range, too many values") raise argparse.ArgumentError(self, msg) class NonNegativeAction(argparse.Action): """A custom action to check whether the value is non-negative or not Ensures the value is >= 0. """ def __call__(self, parser, namespace, values, option_string=None): if int(values) >= 0: setattr(namespace, self.dest, values) else: msg = _("%s expected a non-negative integer") raise argparse.ArgumentTypeError(msg % str(option_string)) osc-lib-2.0.0/osc_lib/cli/__init__.py0000664000175000017500000000000013607371235017352 0ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/cli/identity.py0000664000175000017500000000536313607371235017465 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from openstack import exceptions from openstack.identity.v3 import project from osc_lib.i18n import _ def add_project_owner_option_to_parser(parser): """Register project and project domain options. :param parser: argparse.Argument parser object. """ parser.add_argument( '--project', metavar='', help=_("Owner's project (name or ID)") ) parser.add_argument( '--project-domain', metavar='', help=_('Domain the project belongs to (name or ID). ' 'This can be used in case collisions between project names ' 'exist.'), ) def find_project(sdk_connection, name_or_id, domain_name_or_id=None): """Find a project by its name name or ID. If Forbidden to find the resource (a common case if the user does not have permission), then return the resource by creating a local instance of openstack.identity.v3.Project resource. :param sdk_connection: Connection object of OpenStack SDK. :type sdk_connection: `openstack.connection.Connection` :param name_or_id: Name or ID of the project :type name_or_id: string :param domain_name_or_id: Domain name or ID of the project. This can be used when there are multiple projects with a same name. :returns: the project object found :rtype: `openstack.identity.v3.project.Project` """ try: if domain_name_or_id: domain = sdk_connection.identity.find_domain(domain_name_or_id, ignore_missing=False) domain_id = domain.id else: domain_id = None return sdk_connection.identity.find_project(name_or_id, ignore_missing=False, domain_id=domain_id) # NOTE: OpenStack SDK raises HttpException for 403 response code. # There is no specific exception class at now, so we need to catch # HttpException and check the status code. except exceptions.HttpException as e: if e.status_code == 403: return project.Project(id=name_or_id, name=name_or_id) raise osc-lib-2.0.0/osc_lib/version.py0000664000175000017500000000130613607371235016543 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version __all__ = ['version_info', 'version_string'] version_info = pbr.version.VersionInfo('osc-lib') version_string = version_info.version_string() osc-lib-2.0.0/osc_lib/__init__.py0000664000175000017500000000124113607371235016613 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from osc_lib import version as _version __all__ = ['__version__'] __version__ = _version.version_string osc-lib-2.0.0/osc_lib/shell.py0000664000175000017500000004455113607371235016176 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # Copyright 2015 Dean Troyer # # 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. # """Command-line interface to the OpenStack APIs""" import getpass import locale import logging import sys import traceback from cliff import app from cliff import command from cliff import complete from cliff import help from oslo_utils import importutils from oslo_utils import strutils import six from osc_lib.cli import client_config as cloud_config from osc_lib import clientmanager from osc_lib.command import commandmanager from osc_lib.command import timing from osc_lib import exceptions as exc from osc_lib.i18n import _ from osc_lib import logs from osc_lib import utils from osc_lib import version osprofiler_profiler = importutils.try_import("osprofiler.profiler") DEFAULT_DOMAIN = 'default' DEFAULT_INTERFACE = 'public' def prompt_for_password(prompt=None): """Prompt user for a password Prompt for a password if stdin is a tty. """ if not prompt: prompt = 'Password: ' pw = None # If stdin is a tty, try prompting for the password if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): # Check for Ctl-D try: pw = getpass.getpass(prompt) except EOFError: pass # No password because we did't have a tty or nothing was entered if not pw: raise exc.CommandError(_("No password entered, or found via" " --os-password or OS_PASSWORD"),) return pw class OpenStackShell(app.App): CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' log = logging.getLogger(__name__) timing_data = [] def __init__( self, description=None, version=None, command_manager=None, stdin=None, stdout=None, stderr=None, interactive_app_factory=None, deferred_help=False, ): # Patch command.Command to add a default auth_required = True command.Command.auth_required = True # Some commands do not need authentication help.HelpCommand.auth_required = False complete.CompleteCommand.auth_required = False # Slight change to the meaning of --debug self.DEFAULT_DEBUG_VALUE = None self.DEFAULT_DEBUG_HELP = 'Set debug logging and traceback on errors.' # Do default for positionals if not command_manager: cm = commandmanager.CommandManager('openstack.cli') else: cm = command_manager super(OpenStackShell, self).__init__( description=__doc__.strip(), version=version, command_manager=cm, deferred_help=True, ) # Until we have command line arguments parsed, dump any stack traces self.dump_stack_trace = True # Set in subclasses self.api_version = None self.client_manager = None self.command_options = None self.do_profile = False def configure_logging(self): """Configure logging for the app.""" self.log_configurator = logs.LogConfigurator(self.options) self.dump_stack_trace = self.log_configurator.dump_trace def run(self, argv): ret_val = 1 self.command_options = argv try: ret_val = super(OpenStackShell, self).run(argv) return ret_val except Exception as e: if not logging.getLogger('').handlers: logging.basicConfig() if self.dump_stack_trace: self.log.error(traceback.format_exc()) else: self.log.error('Exception raised: ' + str(e)) return ret_val finally: self.log.info("END return value: %s", ret_val) def init_profile(self): self.do_profile = osprofiler_profiler and self.options.profile if self.do_profile: osprofiler_profiler.init(self.options.profile) def close_profile(self): if self.do_profile: profiler = osprofiler_profiler.get() trace_id = profiler.get_base_id() # Short ID for OpenTracing-based driver (64-bit id) short_id = profiler.get_shorten_id(trace_id) # NOTE(dbelova): let's use warning log level to see these messages # printed. In fact we can define custom log level here with value # bigger than most big default one (CRITICAL) or something like # that (PROFILE = 60 for instance), but not sure we need it here. self.log.warning("Trace ID: %s" % trace_id) self.log.warning("Short trace ID " "for OpenTracing-based drivers: %s" % short_id) self.log.warning("Display trace data with command:\n" "osprofiler trace show --html %s " % trace_id) def run_subcommand(self, argv): self.init_profile() try: ret_value = super(OpenStackShell, self).run_subcommand(argv) finally: self.close_profile() return ret_value def interact(self): self.init_profile() try: ret_value = super(OpenStackShell, self).interact() finally: self.close_profile() return ret_value def build_option_parser(self, description, version): parser = super(OpenStackShell, self).build_option_parser( description, version) # service token auth argument parser.add_argument( '--os-cloud', metavar='', dest='cloud', default=utils.env('OS_CLOUD'), help=_('Cloud name in clouds.yaml (Env: OS_CLOUD)'), ) # Global arguments parser.add_argument( '--os-region-name', metavar='', dest='region_name', default=utils.env('OS_REGION_NAME'), help=_('Authentication region name (Env: OS_REGION_NAME)'), ) parser.add_argument( '--os-cacert', metavar='', dest='cacert', default=utils.env('OS_CACERT', default=None), help=_('CA certificate bundle file (Env: OS_CACERT)'), ) parser.add_argument( '--os-cert', metavar='', dest='cert', default=utils.env('OS_CERT'), help=_('Client certificate bundle file (Env: OS_CERT)'), ) parser.add_argument( '--os-key', metavar='', dest='key', default=utils.env('OS_KEY'), help=_('Client certificate key file (Env: OS_KEY)'), ) verify_group = parser.add_mutually_exclusive_group() verify_group.add_argument( '--verify', action='store_true', default=None, help=_('Verify server certificate (default)'), ) verify_group.add_argument( '--insecure', action='store_true', default=None, help=_('Disable server certificate verification'), ) parser.add_argument( '--os-default-domain', metavar='', dest='default_domain', default=utils.env( 'OS_DEFAULT_DOMAIN', default=DEFAULT_DOMAIN), help=_('Default domain ID, default=%s. ' '(Env: OS_DEFAULT_DOMAIN)') % DEFAULT_DOMAIN, ) parser.add_argument( '--os-interface', metavar='', dest='interface', choices=['admin', 'public', 'internal'], default=utils.env( 'OS_INTERFACE', default=DEFAULT_INTERFACE), help=_('Select an interface type.' ' Valid interface types: [admin, public, internal].' ' default=%s, (Env: OS_INTERFACE)') % DEFAULT_INTERFACE, ) parser.add_argument( '--os-service-provider', metavar='', dest='service_provider', default=utils.env('OS_SERVICE_PROVIDER'), help=_('Authenticate with and perform the command on a service' ' provider using Keystone-to-keystone federation. Must' ' also specify the remote project option.') ) remote_project_group = parser.add_mutually_exclusive_group() remote_project_group.add_argument( '--os-remote-project-name', metavar='', dest='remote_project_name', default=utils.env('OS_REMOTE_PROJECT_NAME'), help=_('Project name when authenticating to a service provider' ' if using Keystone-to-Keystone federation.') ) remote_project_group.add_argument( '--os-remote-project-id', metavar='', dest='remote_project_id', default=utils.env('OS_REMOTE_PROJECT_ID'), help=_('Project ID when authenticating to a service provider' ' if using Keystone-to-Keystone federation.') ) remote_project_domain_group = parser.add_mutually_exclusive_group() remote_project_domain_group.add_argument( '--os-remote-project-domain-name', metavar='', dest='remote_project_domain_name', default=utils.env('OS_REMOTE_PROJECT_DOMAIN_NAME'), help=_('Domain name of the project when authenticating to a' ' service provider if using Keystone-to-Keystone' ' federation.') ) remote_project_domain_group.add_argument( '--os-remote-project-domain-id', metavar='', dest='remote_project_domain_id', default=utils.env('OS_REMOTE_PROJECT_DOMAIN_ID'), help=_('Domain ID of the project when authenticating to a' ' service provider if using Keystone-to-Keystone' ' federation.') ) parser.add_argument( '--timing', default=False, action='store_true', help=_("Print API call timing info"), ) parser.add_argument( '--os-beta-command', action='store_true', help=_("Enable beta commands which are subject to change"), ) # osprofiler HMAC key argument if osprofiler_profiler: parser.add_argument( '--os-profile', metavar='hmac-key', dest='profile', default=utils.env('OS_PROFILE'), help=_('HMAC key for encrypting profiling context data'), ) return parser # return clientmanager.build_plugin_option_parser(parser) """ Break up initialize_app() so that overriding it in a subclass does not require duplicating a lot of the method * super() * _final_defaults() * OpenStackConfig * get_one * _load_plugins() * _load_commands() * ClientManager """ def _final_defaults(self): # Set the default plugin to None # NOTE(dtroyer): This is here to set up for setting it to a default # in the calling CLI self._auth_type = None # Converge project/tenant options project_id = getattr(self.options, 'project_id', None) project_name = getattr(self.options, 'project_name', None) tenant_id = getattr(self.options, 'tenant_id', None) tenant_name = getattr(self.options, 'tenant_name', None) # handle some v2/v3 authentication inconsistencies by just acting like # both the project and tenant information are both present. This can # go away if we stop registering all the argparse options together. if project_id and not tenant_id: self.options.tenant_id = project_id if project_name and not tenant_name: self.options.tenant_name = project_name if tenant_id and not project_id: self.options.project_id = tenant_id if tenant_name and not project_name: self.options.project_name = tenant_name # Save default domain self.default_domain = self.options.default_domain def _load_plugins(self): """Load plugins via stevedore osc-lib has no opinion on what plugins should be loaded """ pass def _load_commands(self): """Load commands via cliff/stevedore osc-lib has no opinion on what commands should be loaded """ pass def initialize_app(self, argv): """Global app init bits: * set up API versions * validate authentication info * authenticate against Identity if requested """ # Parent __init__ parses argv into self.options super(OpenStackShell, self).initialize_app(argv) self.log.info("START with options: %s", strutils.mask_password(" ".join(self.command_options))) self.log.debug("options: %s", strutils.mask_password(self.options)) # Callout for stuff between superclass init and o-c-c self._final_defaults() # Do configuration file handling try: self.cloud_config = cloud_config.OSC_Config( pw_func=prompt_for_password, override_defaults={ 'interface': None, 'auth_type': self._auth_type, }, ) except (IOError, OSError) as e: self.log.critical("Could not read clouds.yaml configuration file") self.print_help_if_requested() raise e # TODO(thowe): Change cliff so the default value for debug # can be set to None. if not self.options.debug: self.options.debug = None # NOTE(dtroyer): Need to do this with validate=False to defer the # auth plugin handling to ClientManager.setup_auth() self.cloud = self.cloud_config.get_one( cloud=self.options.cloud, argparse=self.options, validate=False, ) self.log_configurator.configure(self.cloud) self.dump_stack_trace = self.log_configurator.dump_trace self.log.debug("defaults: %s", self.cloud_config.defaults) self.log.debug("cloud cfg: %s", strutils.mask_password(self.cloud.config)) # Callout for stuff between o-c-c and ClientManager # self._initialize_app_2(self.options) self._load_plugins() self._load_commands() # Handle deferred help and exit self.print_help_if_requested() self.client_manager = clientmanager.ClientManager( cli_options=self.cloud, api_version=self.api_version, pw_func=prompt_for_password, ) def prepare_to_run_command(self, cmd): """Set up auth and API versions""" self.log.info( 'command: %s -> %s.%s (auth=%s)', getattr(cmd, 'cmd_name', ''), cmd.__class__.__module__, cmd.__class__.__name__, cmd.auth_required, ) # NOTE(dtroyer): If auth is not required for a command, skip # get_one()'s validation to avoid loading plugins validate = cmd.auth_required # NOTE(dtroyer): Save the auth required state of the _current_ command # in the ClientManager self.client_manager._auth_required = cmd.auth_required # Validate auth options self.cloud = self.cloud_config.get_one( cloud=self.options.cloud, argparse=self.options, validate=validate, app_name=self.client_manager._app_name, app_version=self.client_manager._app_version, additional_user_agent=[('osc-lib', version.version_string)], ) # Push the updated args into ClientManager self.client_manager._cli_options = self.cloud if cmd.auth_required: self.client_manager.setup_auth() if hasattr(cmd, 'required_scope') and cmd.required_scope: # let the command decide whether we need a scoped token self.client_manager.validate_scope() # Trigger the Identity client to initialize self.client_manager.auth_ref return def clean_up(self, cmd, result, err): self.log.debug('clean_up %s: %s', cmd.__class__.__name__, err or '') # Process collected timing data if self.options.timing: # Get session data self.timing_data.extend( self.client_manager.session.get_timings(), ) # Use the Timing pseudo-command to generate the output tcmd = timing.Timing(self, self.options) tparser = tcmd.get_parser('Timing') # If anything other than prettytable is specified, force csv format = 'table' # Check the formatter used in the actual command if hasattr(cmd, 'formatter') \ and cmd.formatter != cmd._formatter_plugins['table'].obj: format = 'csv' sys.stdout.write('\n') targs = tparser.parse_args(['-f', format]) tcmd.run(targs) def main(argv=None): if argv is None: argv = sys.argv[1:] if six.PY2: # Emulate Py3, decode argv into Unicode based on locale so that # commands always see arguments as text instead of binary data encoding = locale.getpreferredencoding() if encoding: argv = map(lambda arg: arg.decode(encoding), argv) return OpenStackShell().run(argv) if __name__ == "__main__": sys.exit(main(sys.argv[1:])) osc-lib-2.0.0/osc_lib/api/0000775000175000017500000000000013607371310015247 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/api/utils.py0000664000175000017500000000607113607371235016773 0ustar zuulzuul00000000000000# 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. # """API Utilities Library""" def simple_filter( data=None, attr=None, value=None, property_field=None, ): """Filter a list of dicts :param list data: The list to be filtered. The list is modified in-place and will be changed if any filtering occurs. :param string attr: The name of the attribute to filter. If attr does not exist no match will succeed and no rows will be returned. If attr is None no filtering will be performed and all rows will be returned. :param string value: The value to filter. None is considered to be a 'no filter' value. '' matches against a Python empty string. :param string property_field: The name of the data field containing a property dict to filter. If property_field is None, attr is a field name. If property_field is not None, attr is a property key name inside the named property field. :returns: Returns the filtered list :rtype list: This simple filter (one attribute, one exact-match value) searches a list of dicts to select items. It first searches the item dict for a matching ``attr`` then does an exact-match on the ``value``. If ``property_field`` is given, it will look inside that field (if it exists and is a dict) for a matching ``value``. """ # Take the do-nothing case shortcut if not data or not attr or value is None: return data # NOTE:(dtroyer): This filter modifies the provided list in-place using # list.remove() so we need to start at the end so the loop pointer does # not skip any items after a deletion. for d in reversed(data): if attr in d: # Searching data fields search_value = d[attr] elif (property_field and property_field in d and isinstance(d[property_field], dict)): # Searching a properties field - do this separately because # we don't want to fail over to checking the fields if a # property name is given. if attr in d[property_field]: search_value = d[property_field][attr] else: search_value = None else: search_value = None # could do regex here someday... if not search_value or search_value != value: # remove from list try: data.remove(d) except ValueError: # it's already gone! pass return data osc-lib-2.0.0/osc_lib/api/auth.py0000664000175000017500000002001213607371235016563 0ustar zuulzuul00000000000000# 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. # """Authentication Library""" import argparse from keystoneauth1.identity.v3 import k2k from keystoneauth1.loading import base from osc_lib import exceptions as exc from osc_lib.i18n import _ from osc_lib import utils # Initialize the list of Authentication plugins early in order # to get the command-line options PLUGIN_LIST = None # List of plugin command line options OPTIONS_LIST = {} def get_plugin_list(): """Gather plugin list and cache it""" global PLUGIN_LIST if PLUGIN_LIST is None: PLUGIN_LIST = base.get_available_plugin_names() return PLUGIN_LIST def get_options_list(): """Gather plugin options so the help action has them available""" global OPTIONS_LIST if not OPTIONS_LIST: for plugin_name in get_plugin_list(): plugin_options = base.get_plugin_options(plugin_name) for o in plugin_options: os_name = o.name.lower().replace('_', '-') os_env_name = 'OS_' + os_name.upper().replace('-', '_') OPTIONS_LIST.setdefault( os_name, {'env': os_env_name, 'help': ''}, ) # TODO(mhu) simplistic approach, would be better to only add # help texts if they vary from one auth plugin to another # also the text rendering is ugly in the CLI ... OPTIONS_LIST[os_name]['help'] += 'With %s: %s\n' % ( plugin_name, o.help, ) return OPTIONS_LIST def check_valid_authorization_options(options, auth_plugin_name): """Validate authorization options, and provide helpful error messages.""" if (options.auth.get('project_id') and not options.auth.get('domain_id') and not options.auth.get('domain_name') and not options.auth.get('project_name') and not options.auth.get('tenant_id') and not options.auth.get('tenant_name')): raise exc.CommandError(_( 'Missing parameter(s): ' 'Set either a project or a domain scope, but not both. Set a ' 'project scope with --os-project-name, OS_PROJECT_NAME, or ' 'auth.project_name. Alternatively, set a domain scope with ' '--os-domain-name, OS_DOMAIN_NAME or auth.domain_name.' )) def check_valid_authentication_options(options, auth_plugin_name): """Validate authentication options, and provide helpful error messages :param required_scope: indicate whether a scoped token is required """ # Get all the options defined within the plugin. plugin_opts = base.get_plugin_options(auth_plugin_name) plugin_opts = {opt.dest: opt for opt in plugin_opts} # NOTE(aloga): this is an horrible hack. We need a way to specify the # required options in the plugins. Using the "required" argument for # the oslo_config.cfg.Opt does not work, as it is not possible to load the # plugin if the option is not defined, so the error will simply be: # "NoMatchingPlugin: The plugin foobar could not be found" msgs = [] # when no auth params are passed in, user advised to use os-cloud if not options.auth and auth_plugin_name != 'none': msgs.append(_( 'Set a cloud-name with --os-cloud or OS_CLOUD' )) else: if ('password' in plugin_opts and not (options.auth.get('username') or options.auth.get('user_id'))): msgs.append(_( 'Set a username with --os-username, OS_USERNAME,' ' or auth.username' ' or set a user-id with --os-user-id, OS_USER_ID,' ' or auth.user_id' )) if 'auth_url' in plugin_opts and not options.auth.get('auth_url'): msgs.append(_( 'Set an authentication URL, with --os-auth-url,' ' OS_AUTH_URL or auth.auth_url' )) if 'url' in plugin_opts and not options.auth.get('url'): msgs.append(_( 'Set a service URL, with --os-url, OS_URL or auth.url' )) if 'token' in plugin_opts and not options.auth.get('token'): msgs.append(_( 'Set a token with --os-token, OS_TOKEN or auth.token' )) if msgs: raise exc.CommandError( _('Missing parameter(s): \n%s') % '\n'.join(msgs) ) def build_auth_plugins_option_parser(parser): """Auth plugins options builder Builds dynamically the list of options expected by each available authentication plugin. """ available_plugins = list(get_plugin_list()) parser.add_argument( '--os-auth-type', metavar='', dest='auth_type', default=utils.env('OS_AUTH_TYPE'), help=_('Select an authentication type. Available types: %s.' ' Default: selected based on --os-username/--os-token' ' (Env: OS_AUTH_TYPE)') % ', '.join(available_plugins), choices=available_plugins ) # Maintain compatibility with old tenant env vars envs = { 'OS_PROJECT_NAME': utils.env( 'OS_PROJECT_NAME', default=utils.env('OS_TENANT_NAME') ), 'OS_PROJECT_ID': utils.env( 'OS_PROJECT_ID', default=utils.env('OS_TENANT_ID') ), } for o in get_options_list(): # Remove tenant options from KSC plugins and replace them below if 'tenant' not in o: parser.add_argument( '--os-' + o, metavar='' % o, dest=o.replace('-', '_'), default=envs.get( OPTIONS_LIST[o]['env'], utils.env(OPTIONS_LIST[o]['env']), ), help=_('%(help)s\n(Env: %(env)s)') % { 'help': OPTIONS_LIST[o]['help'], 'env': OPTIONS_LIST[o]['env'], }, ) # add tenant-related options for compatibility # this is deprecated but still used in some tempest tests... parser.add_argument( '--os-tenant-name', metavar='', dest='os_project_name', help=argparse.SUPPRESS, ) parser.add_argument( '--os-tenant-id', metavar='', dest='os_project_id', help=argparse.SUPPRESS, ) return parser def get_keystone2keystone_auth(local_auth, service_provider, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None): """Return Keystone 2 Keystone authentication for service provider. :param local_auth: authentication to use with the local Keystone :param service_provider: service provider id as registered in Keystone :param project_id: project id to scope to in the service provider :param project_name: project name to scope to in the service provider :param project_domain_id: id of domain in the service provider :param project_domain_name: name of domain to in the service provider :return: Keystone2Keystone auth object for service provider """ return k2k.Keystone2Keystone(local_auth, service_provider, project_id=project_id, project_name=project_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name) osc-lib-2.0.0/osc_lib/api/__init__.py0000664000175000017500000000000013607371235017354 0ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/api/api.py0000664000175000017500000003117513607371235016407 0ustar zuulzuul00000000000000# 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. # """Base API Library""" import simplejson as json import six from keystoneauth1 import exceptions as ksa_exceptions from keystoneauth1 import session as ksa_session from osc_lib import exceptions from osc_lib.i18n import _ class BaseAPI(object): """Base API wrapper for keystoneauth1.session.Session Encapsulate the translation between keystoneauth1.session.Session and requests.Session in a single layer: * Restore some requests.session.Session compatibility; keystoneauth1.session.Session.request() has the method and url arguments swapped from the rest of the requests-using world. * Provide basic endpoint handling when a Service Catalog is not available. """ # Which service are we? Set in API-specific subclasses SERVICE_TYPE = "" # The common OpenStack microversion header HEADER_NAME = "OpenStack-API-Version" def __init__( self, session=None, service_type=None, endpoint=None, **kwargs ): """Base object that contains some common API objects and methods :param keystoneauth1.session.Session session: The session to be used for making the HTTP API calls. If None, a default keystoneauth1.session.Session will be created. :param string service_type: API name, i.e. ``identity`` or ``compute`` :param string endpoint: An optional URL to be used as the base for API requests on this API. :param kwargs: Keyword arguments passed to keystoneauth1.session.Session(). """ super(BaseAPI, self).__init__() # Create a keystoneauth1.session.Session if one is not supplied if not session: self.session = ksa_session.Session(**kwargs) else: self.session = session self.service_type = service_type self.endpoint = self._munge_endpoint(endpoint) def _munge_endpoint(self, endpoint): """Hook to allow subclasses to massage the passed-in endpoint Hook to massage passed-in endpoints from arbitrary sources, including direct user input. By default just remove trailing '/' as all of our path info strings start with '/' and not all services can handle '//' in their URLs. Some subclasses will override this to do additional work, most likely with regard to API versions. :param string endpoint: The service endpoint, generally direct from the service catalog. :return: The modified endpoint """ if isinstance(endpoint, six.string_types): return endpoint.rstrip('/') else: return endpoint def _request(self, method, url, session=None, **kwargs): """Perform call into session All API calls are funneled through this method to provide a common place to finalize the passed URL and other things. :param string method: The HTTP method name, i.e. ``GET``, ``PUT``, etc :param string url: The API-specific portion of the URL path, or a full URL if ``endpoint`` was not supplied at initialization. :param keystoneauth1.session.Session session: An initialized session to override the one created at initialization. :param kwargs: Keyword arguments passed to requests.request(). :return: the requests.Response object """ # If session arg is supplied, use it this time, but don't save it if not session: session = self.session # Do the auto-endpoint magic if self.endpoint: if url: url = '/'.join([self.endpoint.rstrip('/'), url.lstrip('/')]) else: # NOTE(dtroyer): This is left here after _munge_endpoint() is # added because endpoint is public and there is # no accounting for what may happen. url = self.endpoint.rstrip('/') else: # Pass on the lack of URL unmolested to maintain the same error # handling from keystoneauth: raise EndpointNotFound pass # Hack out empty headers 'cause KSA can't stomach it if 'headers' in kwargs and kwargs['headers'] is None: kwargs.pop('headers') # Why is ksc session backwards??? return session.request(url, method, **kwargs) # The basic action methods all take a Session and return dict/lists def create( self, url, session=None, method=None, **params ): """Create a new resource :param string url: The API-specific portion of the URL path :param Session session: HTTP client session :param string method: HTTP method (default POST) """ if not method: method = 'POST' ret = self._request(method, url, session=session, **params) # Should this move into _requests()? try: return ret.json() except json.JSONDecodeError: return ret def delete( self, url, session=None, **params ): """Delete a resource :param string url: The API-specific portion of the URL path :param Session session: HTTP client session """ return self._request('DELETE', url, **params) def list( self, path, session=None, body=None, detailed=False, headers=None, **params ): """Return a list of resources GET ${ENDPOINT}/${PATH}?${PARAMS} path is often the object's plural resource type :param string path: The API-specific portion of the URL path :param Session session: HTTP client session :param body: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param bool detailed: Adds '/details' to path for some APIs to return extended attributes :param dict headers: Headers dictionary to pass to requests :returns: JSON-decoded response, could be a list or a dict-wrapped-list """ if detailed: path = '/'.join([path.rstrip('/'), 'details']) if body: ret = self._request( 'POST', path, # service=self.service_type, json=body, params=params, headers=headers, ) else: ret = self._request( 'GET', path, # service=self.service_type, params=params, headers=headers, ) try: return ret.json() except json.JSONDecodeError: return ret # Layered actions built on top of the basic action methods do not # explicitly take a Session but one may still be passed in kwargs def find_attr( self, path, value=None, attr=None, resource=None, ): """Find a resource via attribute or ID Most APIs return a list wrapped by a dict with the resource name as key. Some APIs (Identity) return a dict when a query string is present and there is one return value. Take steps to unwrap these bodies and return a single dict without any resource wrappers. :param string path: The API-specific portion of the URL path :param string value: value to search for :param string attr: attribute to use for resource search :param string resource: plural of the object resource name; defaults to path For example: n = find(netclient, 'network', 'networks', 'matrix') """ # Default attr is 'name' if attr is None: attr = 'name' # Default resource is path - in many APIs they are the same if resource is None: resource = path def getlist(kw): """Do list call, unwrap resource dict if present""" ret = self.list(path, **kw) if isinstance(ret, dict) and resource in ret: ret = ret[resource] return ret # Search by attribute kwargs = {attr: value} data = getlist(kwargs) if isinstance(data, dict): return data if len(data) == 1: return data[0] if len(data) > 1: msg = _("Multiple %(resource)s exist with %(attr)s='%(value)s'") raise exceptions.CommandError( msg % {'resource': resource, 'attr': attr, 'value': value} ) # Search by id kwargs = {'id': value} data = getlist(kwargs) if len(data) == 1: return data[0] msg = _("No %(resource)s with a %(attr)s or ID of '%(value)s' found") raise exceptions.CommandError( msg % {'resource': resource, 'attr': attr, 'value': value} ) def find_bulk( self, path, headers=None, **kwargs ): """Bulk load and filter locally :param string path: The API-specific portion of the URL path :param kwargs: A dict of AVPs to match - logical AND :param dict headers: Headers dictionary to pass to requests :returns: list of resource dicts """ items = self.list(path) if isinstance(items, dict): # strip off the enclosing dict key = list(items.keys())[0] items = items[key] ret = [] for o in items: try: if all(o[attr] == kwargs[attr] for attr in kwargs.keys()): ret.append(o) except KeyError: continue return ret def find_one( self, path, **kwargs ): """Find a resource by name or ID :param string path: The API-specific portion of the URL path :returns: resource dict """ bulk_list = self.find_bulk(path, **kwargs) num_bulk = len(bulk_list) if num_bulk == 0: msg = _("none found") raise exceptions.NotFound(404, msg) elif num_bulk > 1: msg = _("many found") raise RuntimeError(msg) return bulk_list[0] def find( self, path, value=None, attr=None, headers=None, ): """Find a single resource by name or ID :param string path: The API-specific portion of the URL path :param string value: search expression (required, really) :param string attr: name of attribute for secondary search :param dict headers: Headers dictionary to pass to requests """ def raise_not_found(): msg = _("%s not found") % value raise exceptions.NotFound(404, msg) try: ret = self._request( 'GET', "/%s/%s" % (path, value), headers=headers, ).json() if isinstance(ret, dict): # strip off the enclosing dict key = list(ret.keys())[0] ret = ret[key] except ( ksa_exceptions.NotFound, ksa_exceptions.BadRequest, ): if attr: kwargs = {attr: value} try: ret = self.find_one( path, headers=headers, **kwargs ) except ( exceptions.NotFound, ksa_exceptions.NotFound, ): raise_not_found() else: raise_not_found() return ret osc-lib-2.0.0/osc_lib/tests/0000775000175000017500000000000013607371310015640 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/tests/command/0000775000175000017500000000000013607371310017256 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/tests/command/test_timing.py0000664000175000017500000000562213607371235022171 0ustar zuulzuul00000000000000# 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. # """Test Timing pseudo-command""" import datetime from keystoneauth1 import session from osc_lib.command import timing from osc_lib.tests import fakes from osc_lib.tests import utils timing_method = 'GET' timing_url = 'http://localhost:5000' timing_elapsed = 0.872809 class FakeGenericClient(object): def __init__(self, **kwargs): self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] class TestTiming(utils.TestCommand): columns = ( 'URL', 'Seconds', ) def setUp(self): super(TestTiming, self).setUp() self.app.timing_data = [] self.app.client_manager.compute = FakeGenericClient( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) self.app.client_manager.volume = FakeGenericClient( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) # Get the command object to test self.cmd = timing.Timing(self.app, None) def test_timing_list_no_data(self): arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # In base command class Lister in cliff, abstract method take_action() # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) datalist = [ ('Total', 0.0,) ] self.assertEqual(datalist, data) def test_timing_list(self): self.app.timing_data = [session.RequestTiming( method=timing_method, url=timing_url, elapsed=datetime.timedelta(microseconds=timing_elapsed * 1000000), )] arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # In base command class Lister in cliff, abstract method take_action() # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) datalist = [ (timing_method + ' ' + timing_url, timing_elapsed), ('Total', timing_elapsed), ] self.assertEqual(datalist, data) osc-lib-2.0.0/osc_lib/tests/command/test_command.py0000664000175000017500000000320613607371235022314 0ustar zuulzuul00000000000000# Copyright 2016 NEC Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import mock from osc_lib.command import command from osc_lib import exceptions from osc_lib.tests import fakes as test_fakes from osc_lib.tests import utils as test_utils class FakeCommand(command.Command): def take_action(self, parsed_args): pass class TestCommand(test_utils.TestCase): def test_command_has_logger(self): cmd = FakeCommand(mock.Mock(), mock.Mock()) self.assertTrue(hasattr(cmd, 'log')) self.assertEqual( 'osc_lib.tests.command.test_command.FakeCommand', cmd.log.name, ) def test_validate_os_beta_command_enabled(self): cmd = FakeCommand(mock.Mock(), mock.Mock()) cmd.app = mock.Mock() cmd.app.options = test_fakes.FakeOptions() # No exception is raised when enabled. cmd.app.options.os_beta_command = True cmd.validate_os_beta_command_enabled() cmd.app.options.os_beta_command = False self.assertRaises( exceptions.CommandError, cmd.validate_os_beta_command_enabled, ) osc-lib-2.0.0/osc_lib/tests/command/__init__.py0000664000175000017500000000000013607371235021363 0ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/tests/command/test_commandmanager.py0000664000175000017500000000656713607371235023664 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import mock from osc_lib.command import commandmanager from osc_lib.tests import utils class FakeCommand(object): @classmethod def load(cls): return cls def __init__(self): return FAKE_CMD_ONE = FakeCommand FAKE_CMD_TWO = FakeCommand FAKE_CMD_ALPHA = FakeCommand FAKE_CMD_BETA = FakeCommand class FakeCommandManager(commandmanager.CommandManager): commands = {} def load_commands(self, namespace): if namespace == 'test': self.commands['one'] = FAKE_CMD_ONE self.commands['two'] = FAKE_CMD_TWO self.group_list.append(namespace) elif namespace == 'greek': self.commands['alpha'] = FAKE_CMD_ALPHA self.commands['beta'] = FAKE_CMD_BETA self.group_list.append(namespace) class TestCommandManager(utils.TestCase): def test_add_command_group(self): mgr = FakeCommandManager('test') # Make sure add_command() still functions mock_cmd_one = mock.Mock() mgr.add_command('mock', mock_cmd_one) cmd_mock, name, args = mgr.find_command(['mock']) self.assertEqual(mock_cmd_one, cmd_mock) # Find a command added in initialization cmd_one, name, args = mgr.find_command(['one']) self.assertEqual(FAKE_CMD_ONE, cmd_one) # Load another command group mgr.add_command_group('greek') # Find a new command cmd_alpha, name, args = mgr.find_command(['alpha']) self.assertEqual(FAKE_CMD_ALPHA, cmd_alpha) # Ensure that the original commands were not overwritten cmd_two, name, args = mgr.find_command(['two']) self.assertEqual(FAKE_CMD_TWO, cmd_two) def test_get_command_groups(self): mgr = FakeCommandManager('test') # Make sure add_command() still functions mock_cmd_one = mock.Mock() mgr.add_command('mock', mock_cmd_one) cmd_mock, name, args = mgr.find_command(['mock']) self.assertEqual(mock_cmd_one, cmd_mock) # Load another command group mgr.add_command_group('greek') gl = mgr.get_command_groups() self.assertEqual(['test', 'greek'], gl) def test_get_command_names(self): mock_cmd_one = mock.Mock() mock_cmd_one.name = 'one' mock_cmd_two = mock.Mock() mock_cmd_two.name = 'cmd two' mock_pkg_resources = mock.Mock( return_value=[mock_cmd_one, mock_cmd_two], ) with mock.patch( 'pkg_resources.iter_entry_points', mock_pkg_resources, ) as iter_entry_points: mgr = commandmanager.CommandManager('test') iter_entry_points.assert_called_once_with('test') cmds = mgr.get_command_names('test') self.assertEqual(['one', 'cmd two'], cmds) osc-lib-2.0.0/osc_lib/tests/test_logs.py0000664000175000017500000002075313607371235020232 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import logging import mock from osc_lib import logs from osc_lib.tests import utils class TestContext(utils.TestCase): def test_log_level_from_options(self): opts = mock.Mock() opts.verbose_level = 0 self.assertEqual(logging.ERROR, logs.log_level_from_options(opts)) opts.verbose_level = 1 self.assertEqual(logging.WARNING, logs.log_level_from_options(opts)) opts.verbose_level = 2 self.assertEqual(logging.INFO, logs.log_level_from_options(opts)) opts.verbose_level = 3 self.assertEqual(logging.DEBUG, logs.log_level_from_options(opts)) def test_log_level_from_config(self): cfg = {'verbose_level': 0} self.assertEqual(logging.ERROR, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1} self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 2} self.assertEqual(logging.INFO, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 3} self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'critical'} self.assertEqual(logging.CRITICAL, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'error'} self.assertEqual(logging.ERROR, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'warning'} self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'info'} self.assertEqual(logging.INFO, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'debug'} self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'bogus'} self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'info', 'debug': True} self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) @mock.patch('warnings.simplefilter') def test_set_warning_filter(self, simplefilter): logs.set_warning_filter(logging.ERROR) simplefilter.assert_called_with("ignore") logs.set_warning_filter(logging.WARNING) simplefilter.assert_called_with("ignore") logs.set_warning_filter(logging.INFO) simplefilter.assert_called_with("once") class TestFileFormatter(utils.TestCase): def test_nothing(self): formatter = logs._FileFormatter() self.assertEqual(('%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' '%(name)s %(message)s'), formatter.fmt) def test_options(self): class Opts(object): cloud = 'cloudy' os_project_name = 'projecty' username = 'usernamey' options = Opts() formatter = logs._FileFormatter(options=options) self.assertEqual(('%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' '%(name)s [cloudy usernamey projecty] %(message)s'), formatter.fmt) def test_config(self): config = mock.Mock() config.config = {'cloud': 'cloudy'} config.auth = {'project_name': 'projecty', 'username': 'usernamey'} formatter = logs._FileFormatter(config=config) self.assertEqual(('%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' '%(name)s [cloudy usernamey projecty] %(message)s'), formatter.fmt) class TestLogConfigurator(utils.TestCase): def setUp(self): super(TestLogConfigurator, self).setUp() self.options = mock.Mock() self.options.verbose_level = 1 self.options.log_file = None self.options.debug = False self.root_logger = mock.Mock() self.root_logger.setLevel = mock.Mock() self.root_logger.addHandler = mock.Mock() self.requests_log = mock.Mock() self.requests_log.setLevel = mock.Mock() self.cliff_log = mock.Mock() self.cliff_log.setLevel = mock.Mock() self.stevedore_log = mock.Mock() self.stevedore_log.setLevel = mock.Mock() self.iso8601_log = mock.Mock() self.iso8601_log.setLevel = mock.Mock() self.loggers = [ self.root_logger, self.requests_log, self.cliff_log, self.stevedore_log, self.iso8601_log] @mock.patch('logging.StreamHandler') @mock.patch('logging.getLogger') @mock.patch('osc_lib.logs.set_warning_filter') def test_init(self, warning_filter, getLogger, handle): getLogger.side_effect = self.loggers console_logger = mock.Mock() console_logger.setFormatter = mock.Mock() console_logger.setLevel = mock.Mock() handle.return_value = console_logger configurator = logs.LogConfigurator(self.options) getLogger.assert_called_with('iso8601') # last call warning_filter.assert_called_with(logging.WARNING) self.root_logger.setLevel.assert_called_with(logging.DEBUG) self.root_logger.addHandler.assert_called_with(console_logger) self.requests_log.setLevel.assert_called_with(logging.ERROR) self.cliff_log.setLevel.assert_called_with(logging.ERROR) self.stevedore_log.setLevel.assert_called_with(logging.ERROR) self.iso8601_log.setLevel.assert_called_with(logging.ERROR) self.assertFalse(configurator.dump_trace) @mock.patch('logging.getLogger') @mock.patch('osc_lib.logs.set_warning_filter') def test_init_no_debug(self, warning_filter, getLogger): getLogger.side_effect = self.loggers self.options.debug = True configurator = logs.LogConfigurator(self.options) warning_filter.assert_called_with(logging.DEBUG) self.requests_log.setLevel.assert_called_with(logging.DEBUG) self.assertTrue(configurator.dump_trace) @mock.patch('logging.FileHandler') @mock.patch('logging.getLogger') @mock.patch('osc_lib.logs.set_warning_filter') @mock.patch('osc_lib.logs._FileFormatter') def test_init_log_file(self, formatter, warning_filter, getLogger, handle): getLogger.side_effect = self.loggers self.options.log_file = '/tmp/log_file' file_logger = mock.Mock() file_logger.setFormatter = mock.Mock() file_logger.setLevel = mock.Mock() handle.return_value = file_logger mock_formatter = mock.Mock() formatter.return_value = mock_formatter logs.LogConfigurator(self.options) handle.assert_called_with(filename=self.options.log_file) self.root_logger.addHandler.assert_called_with(file_logger) file_logger.setFormatter.assert_called_with(mock_formatter) file_logger.setLevel.assert_called_with(logging.WARNING) @mock.patch('logging.FileHandler') @mock.patch('logging.getLogger') @mock.patch('osc_lib.logs.set_warning_filter') @mock.patch('osc_lib.logs._FileFormatter') def test_configure(self, formatter, warning_filter, getLogger, handle): getLogger.side_effect = self.loggers configurator = logs.LogConfigurator(self.options) cloud_config = mock.Mock() config_log = '/tmp/config_log' cloud_config.config = { 'log_file': config_log, 'verbose_level': 1, 'log_level': 'info'} file_logger = mock.Mock() file_logger.setFormatter = mock.Mock() file_logger.setLevel = mock.Mock() handle.return_value = file_logger mock_formatter = mock.Mock() formatter.return_value = mock_formatter configurator.configure(cloud_config) warning_filter.assert_called_with(logging.INFO) handle.assert_called_with(filename=config_log) self.root_logger.addHandler.assert_called_with(file_logger) file_logger.setFormatter.assert_called_with(mock_formatter) file_logger.setLevel.assert_called_with(logging.INFO) self.assertFalse(configurator.dump_trace) osc-lib-2.0.0/osc_lib/tests/utils/0000775000175000017500000000000013607371310017000 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/tests/utils/test_tags.py0000664000175000017500000002644413607371235021367 0ustar zuulzuul00000000000000# Copyright 2019 Red Hat, Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import mock from osc_lib.tests import utils as test_utils from osc_lib.utils import tags def help_enhancer(_h): """A simple helper to validate the ``enhance_help`` kwarg.""" return ''.join(reversed(_h)) class TestTags(test_utils.TestCase): def test_add_tag_filtering_option_to_parser(self): parser = argparse.ArgumentParser() tags.add_tag_filtering_option_to_parser(parser, 'test') parsed_args = parser.parse_args(['--tags', 'tag1,tag2', '--any-tags', 'tag4', '--not-tags', 'tag5', '--not-any-tags', 'tag6']) actual = getattr(parsed_args, 'tags', []) expected = ['tag1', 'tag2'] self.assertItemsEqual(expected, actual) actual = getattr(parsed_args, 'any_tags', []) expected = ['tag4'] self.assertItemsEqual(expected, actual) actual = getattr(parsed_args, 'not_tags', []) expected = ['tag5'] self.assertItemsEqual(expected, actual) actual = getattr(parsed_args, 'not_any_tags', []) expected = ['tag6'] self.assertItemsEqual(expected, actual) def test_get_tag_filtering_args(self): parser = argparse.ArgumentParser() tags.add_tag_filtering_option_to_parser(parser, 'test') parsed_args = parser.parse_args(['--tags', 'tag1,tag2', '--any-tags', 'tag4', '--not-tags', 'tag5', '--not-any-tags', 'tag6']) expected = {'tags': 'tag1,tag2', 'any_tags': 'tag4', 'not_tags': 'tag5', 'not_any_tags': 'tag6'} args = {} tags.get_tag_filtering_args(parsed_args, args) self.assertEqual(expected, args) def test_add_tag_option_to_parser_for_create(self): parser = argparse.ArgumentParser() tags.add_tag_option_to_parser_for_create(parser, 'test') # Test that --tag and --no-tag are mutually exclusive self.assertRaises(SystemExit, parser.parse_args, ['--tag', 'tag1', '--no-tag']) parsed_args = parser.parse_args(['--tag', 'tag1']) actual = getattr(parsed_args, 'tags', []) expected = ['tag1'] self.assertItemsEqual(expected, actual) parsed_args = parser.parse_args(['--no-tag']) actual = getattr(parsed_args, 'no-tag', []) expected = [] self.assertItemsEqual(expected, actual) def test_add_tag_option_to_parser_for_set(self): parser = argparse.ArgumentParser() tags.add_tag_option_to_parser_for_set(parser, 'test') parsed_args = parser.parse_args(['--tag', 'tag1']) actual = getattr(parsed_args, 'tags', []) expected = ['tag1'] self.assertItemsEqual(expected, actual) parsed_args = parser.parse_args(['--no-tag']) actual = getattr(parsed_args, 'no-tag', []) expected = [] self.assertItemsEqual(expected, actual) def test_add_tag_option_to_parser_for_unset(self): parser = argparse.ArgumentParser() tags.add_tag_option_to_parser_for_unset(parser, 'test') # Test that --tag and --all-tag are mutually exclusive self.assertRaises(SystemExit, parser.parse_args, ['--tag', 'tag1', '--all-tag']) parsed_args = parser.parse_args(['--tag', 'tag1']) actual = getattr(parsed_args, 'tags', []) expected = ['tag1'] self.assertItemsEqual(expected, actual) parsed_args = parser.parse_args(['--all-tag']) actual = getattr(parsed_args, 'all-tag', []) expected = [] self.assertItemsEqual(expected, actual) def test_update_tags_for_set(self): mock_client = mock.MagicMock() mock_obj = mock.MagicMock() mock_parsed_args = mock.MagicMock() # no-tag True path mock_parsed_args.no_tag = True mock_parsed_args.tags = ['tag1'] mock_obj.tags = None tags.update_tags_for_set(mock_client, mock_obj, mock_parsed_args) mock_client.set_tags.assert_called_once_with( mock_obj, list(mock_parsed_args.tags)) # no-tag False path mock_client.set_tags.reset_mock() mock_parsed_args.no_tag = False mock_parsed_args.tags = ['tag1'] mock_obj.tags = ['tag2'] expected_list = ['tag1', 'tag2'] tags.update_tags_for_set(mock_client, mock_obj, mock_parsed_args) mock_client.set_tags.assert_called_once_with( mock_obj, expected_list) # no new tags path mock_client.set_tags.reset_mock() mock_parsed_args.no_tag = False mock_parsed_args.tags = None mock_obj.tags = ['tag2'] tags.update_tags_for_set(mock_client, mock_obj, mock_parsed_args) mock_client.set_tags.assert_not_called() def test_update_tags_for_unset(self): mock_client = mock.MagicMock() mock_obj = mock.MagicMock() mock_parsed_args = mock.MagicMock() # No new tags mock_obj.tags = ['tag1'] mock_parsed_args.all_tag = False mock_parsed_args.tags = None tags.update_tags_for_unset(mock_client, mock_obj, mock_parsed_args) mock_client.set_tags.assert_not_called() # Clear all tags mock_obj.tags = ['tag1'] mock_parsed_args.all_tag = True mock_parsed_args.tags = None tags.update_tags_for_unset(mock_client, mock_obj, mock_parsed_args) mock_client.set_tags.assert_called_once_with( mock_obj, []) # Remove one tag mock_client.set_tags.reset_mock() mock_obj.tags = ['tag1', 'tag2'] mock_parsed_args.all_tag = False mock_parsed_args.tags = ['tag2'] tags.update_tags_for_unset(mock_client, mock_obj, mock_parsed_args) mock_client.set_tags.assert_called_once_with( mock_obj, ['tag1']) class TestTagHelps(test_utils.TestCase): def _test_tag_method_help(self, meth, exp_normal, exp_enhanced): """Vet the help text of the options added by the tag filtering helpers. :param meth: One of the ``add_tag_*`` methods. :param exp_normal: Expected help output without ``enhance_help``. :param exp_enhanced: Expected output with ``enhance_help`` set to ``help_enhancer`` """ parser = argparse.ArgumentParser() meth(parser, 'test') self.assertEqual(exp_normal, parser.format_help()) parser = argparse.ArgumentParser() meth(parser, 'test', enhance_help=help_enhancer) self.assertEqual(exp_enhanced, parser.format_help()) def test_add_tag_filtering_option_to_parser(self): self._test_tag_method_help( tags.add_tag_filtering_option_to_parser, """\ usage: run.py [-h] [--tags [,,...]] [--any-tags [,,...]] [--not-tags [,,...]] [--not-any-tags [,,...]] optional arguments: -h, --help show this help message and exit --tags [,,...] List test which have all given tag(s) (Comma-separated list of tags) --any-tags [,,...] List test which have any given tag(s) (Comma-separated list of tags) --not-tags [,,...] Exclude test which have all given tag(s) (Comma- separated list of tags) --not-any-tags [,,...] Exclude test which have any given tag(s) (Comma- separated list of tags) """, """\ usage: run.py [-h] [--tags [,,...]] [--any-tags [,,...]] [--not-tags [,,...]] [--not-any-tags [,,...]] optional arguments: -h, --help show this help message and exit --tags [,,...] )sgat fo tsil detarapes-ammoC( )s(gat nevig lla evah hcihw tset tsiL --any-tags [,,...] )sgat fo tsil detarapes-ammoC( )s(gat nevig yna evah hcihw tset tsiL --not-tags [,,...] )sgat fo tsil detarapes-ammoC( )s(gat nevig lla evah hcihw tset edulcxE --not-any-tags [,,...] )sgat fo tsil detarapes-ammoC( )s(gat nevig yna evah hcihw tset edulcxE """) def test_add_tag_option_to_parser_for_create(self): self._test_tag_method_help( tags.add_tag_option_to_parser_for_create, """\ usage: run.py [-h] [--tag | --no-tag] optional arguments: -h, --help show this help message and exit --tag Tag to be added to the test (repeat option to set multiple tags) --no-tag No tags associated with the test """, """\ usage: run.py [-h] [--tag | --no-tag] optional arguments: -h, --help show this help message and exit --tag )sgat elpitlum tes ot noitpo taeper( tset eht ot dedda eb ot gaT --no-tag tset eht htiw detaicossa sgat oN """) def test_add_tag_option_to_parser_for_set(self): self._test_tag_method_help( tags.add_tag_option_to_parser_for_set, """\ usage: run.py [-h] [--tag ] [--no-tag] optional arguments: -h, --help show this help message and exit --tag Tag to be added to the test (repeat option to set multiple tags) --no-tag Clear tags associated with the test. Specify both --tag and --no-tag to overwrite current tags """, """\ usage: run.py [-h] [--tag ] [--no-tag] optional arguments: -h, --help show this help message and exit --tag )sgat elpitlum tes ot noitpo taeper( tset eht ot dedda eb ot gaT --no-tag sgat tnerruc etirwrevo ot gat-on-- dna gat-- htob yficepS .tset eht htiw detaicossa sgat raelC """) def test_add_tag_option_to_parser_for_unset(self): self._test_tag_method_help( tags.add_tag_option_to_parser_for_unset, """\ usage: run.py [-h] [--tag | --all-tag] optional arguments: -h, --help show this help message and exit --tag Tag to be removed from the test (repeat option to remove multiple tags) --all-tag Clear all tags associated with the test """, """\ usage: run.py [-h] [--tag | --all-tag] optional arguments: -h, --help show this help message and exit --tag )sgat elpitlum evomer ot noitpo taeper( tset eht morf devomer eb ot gaT --all-tag tset eht htiw detaicossa sgat lla raelC """) osc-lib-2.0.0/osc_lib/tests/utils/test_columns.py0000664000175000017500000000457413607371235022111 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from osc_lib.tests import utils as test_utils from osc_lib.utils import columns as column_utils class TestColumnUtils(test_utils.TestCase): def test_get_column_definitions(self): attr_map = ( ('id', 'ID', column_utils.LIST_BOTH), ('tenant_id', 'Project', column_utils.LIST_LONG_ONLY), ('name', 'Name', column_utils.LIST_BOTH), ('summary', 'Summary', column_utils.LIST_SHORT_ONLY), ) headers, columns = column_utils.get_column_definitions( attr_map, long_listing=False) self.assertEqual(['id', 'name', 'summary'], columns) self.assertEqual(['ID', 'Name', 'Summary'], headers) def test_get_column_definitions_long(self): attr_map = ( ('id', 'ID', column_utils.LIST_BOTH), ('tenant_id', 'Project', column_utils.LIST_LONG_ONLY), ('name', 'Name', column_utils.LIST_BOTH), ('summary', 'Summary', column_utils.LIST_SHORT_ONLY), ) headers, columns = column_utils.get_column_definitions( attr_map, long_listing=True) self.assertEqual(['id', 'tenant_id', 'name'], columns) self.assertEqual(['ID', 'Project', 'Name'], headers) def test_get_columns(self): item = { 'id': 'test-id', 'tenant_id': 'test-tenant_id', # 'name' is not included 'foo': 'bar', # unknown attribute } attr_map = ( ('id', 'ID', column_utils.LIST_BOTH), ('tenant_id', 'Project', column_utils.LIST_LONG_ONLY), ('name', 'Name', column_utils.LIST_BOTH), ) columns, display_names = column_utils.get_columns(item, attr_map) self.assertEqual(tuple(['id', 'tenant_id', 'foo']), columns) self.assertEqual(tuple(['ID', 'Project', 'foo']), display_names) osc-lib-2.0.0/osc_lib/tests/utils/__init__.py0000664000175000017500000003437613607371235021134 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # Copyright 2013 Nebula Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import contextlib import copy import json as jsonutils import mock import os from cliff import columns as cliff_columns import fixtures from keystoneauth1 import loading from openstack.config import cloud_region from openstack.config import defaults from oslo_utils import importutils from requests_mock.contrib import fixture import six import testtools from osc_lib import clientmanager from osc_lib import shell from osc_lib.tests import fakes def fake_execute(shell, cmd): """Pretend to execute shell commands.""" return shell.run(cmd.split()) def make_shell(shell_class=None): """Create a new command shell and mock out some bits.""" if shell_class is None: shell_class = shell.OpenStackShell _shell = shell_class() _shell.command_manager = mock.Mock() # _shell.cloud = mock.Mock() return _shell def opt2attr(opt): if opt.startswith('--os-'): attr = opt[5:] elif opt.startswith('--'): attr = opt[2:] else: attr = opt return attr.lower().replace('-', '_') def opt2env(opt): return opt[2:].upper().replace('-', '_') class EnvFixture(fixtures.Fixture): """Environment Fixture. This fixture replaces os.environ with provided env or an empty env. """ def __init__(self, env=None): self.new_env = env or {} def _setUp(self): self.orig_env, os.environ = os.environ, self.new_env self.addCleanup(self.revert) def revert(self): os.environ = self.orig_env class ParserException(Exception): pass class TestCase(testtools.TestCase): def setUp(self): testtools.TestCase.setUp(self) if (os.environ.get("OS_STDOUT_CAPTURE") == "True" or os.environ.get("OS_STDOUT_CAPTURE") == "1"): stdout = self.useFixture(fixtures.StringStream("stdout")).stream self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout)) if (os.environ.get("OS_STDERR_CAPTURE") == "True" or os.environ.get("OS_STDERR_CAPTURE") == "1"): stderr = self.useFixture(fixtures.StringStream("stderr")).stream self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr)) def assertNotCalled(self, m, msg=None): """Assert a function was not called""" if m.called: if not msg: msg = 'method %s should not have been called' % m self.fail(msg) @contextlib.contextmanager def subTest(self, *args, **kwargs): """This is a wrapper to unittest's subTest method. This wrapper suppresses 2 issues: * lack of support in older Python versions * bug in testtools that breaks support for all versions """ try: with super(TestCase, self).subTest(*args, **kwargs): yield except TypeError: # NOTE(elhararb): subTest is supported by unittest only from PY3.4 if six.PY2: yield else: raise except AttributeError: # TODO(elhararb): remove this except clause when subTest is # enabled in testtools yield class TestCommand(TestCase): """Test cliff command classes""" def setUp(self): super(TestCommand, self).setUp() # Build up a fake app self.fake_stdout = fakes.FakeStdout() self.fake_log = fakes.FakeLog() self.app = fakes.FakeApp(self.fake_stdout, self.fake_log) self.app.client_manager = fakes.FakeClientManager() def check_parser(self, cmd, args, verify_args): cmd_parser = cmd.get_parser('check_parser') try: parsed_args = cmd_parser.parse_args(args) except SystemExit: raise ParserException("Argument parse failed") for av in verify_args: attr, value = av if attr: self.assertIn(attr, parsed_args) self.assertEqual(value, getattr(parsed_args, attr)) return parsed_args def assertItemEqual(self, expected, actual): """Compare item considering formattable columns. This method compares an observed item to an expected item column by column. If a column is a formattable column, observed and expected columns are compared using human_readable() and machine_readable(). """ self.assertEqual(len(expected), len(actual)) for col_expected, col_actual in zip(expected, actual): if isinstance(col_expected, cliff_columns.FormattableColumn): self.assertIsInstance(col_actual, col_expected.__class__) self.assertEqual(col_expected.human_readable(), col_actual.human_readable()) self.assertEqual(col_expected.machine_readable(), col_actual.machine_readable()) else: self.assertEqual(col_expected, col_actual) def assertListItemEqual(self, expected, actual): """Compare a list of items considering formattable columns. Each pair of observed and expected items are compared using assertItemEqual() method. """ self.assertEqual(len(expected), len(actual)) for item_expected, item_actual in zip(expected, actual): self.assertItemEqual(item_expected, item_actual) class TestClientManager(TestCase): """ClientManager class test framework""" default_password_auth = { 'auth_url': fakes.AUTH_URL, 'username': fakes.USERNAME, 'password': fakes.PASSWORD, 'project_name': fakes.PROJECT_NAME, } default_token_auth = { 'auth_url': fakes.AUTH_URL, 'token': fakes.AUTH_TOKEN, } def setUp(self): super(TestClientManager, self).setUp() self.mock = mock.Mock() self.requests = self.useFixture(fixture.Fixture()) # fake v2password token retrieval self.stub_auth(json=fakes.TEST_RESPONSE_DICT) # fake token and token_endpoint retrieval self.stub_auth(json=fakes.TEST_RESPONSE_DICT, url='/'.join([fakes.AUTH_URL, 'v2.0/tokens'])) # fake v3password token retrieval self.stub_auth(json=fakes.TEST_RESPONSE_DICT_V3, url='/'.join([fakes.AUTH_URL, 'v3/auth/tokens'])) # fake password token retrieval self.stub_auth(json=fakes.TEST_RESPONSE_DICT_V3, url='/'.join([fakes.AUTH_URL, 'auth/tokens'])) # fake password version endpoint discovery self.stub_auth(json=fakes.TEST_VERSIONS, url=fakes.AUTH_URL, verb='GET') # Mock the auth plugin self.auth_mock = mock.Mock() def stub_auth(self, json=None, url=None, verb=None, **kwargs): subject_token = fakes.AUTH_TOKEN base_url = fakes.AUTH_URL if json: text = jsonutils.dumps(json) headers = { 'X-Subject-Token': subject_token, 'Content-Type': 'application/json', } if not url: url = '/'.join([base_url, 'tokens']) url = url.replace("/?", "?") if not verb: verb = 'POST' self.requests.register_uri( verb, url, headers=headers, text=text, ) def _clientmanager_class(self): """Allow subclasses to override the ClientManager class""" return clientmanager.ClientManager def _make_clientmanager( self, auth_args=None, config_args=None, identity_api_version=None, auth_plugin_name=None, auth_required=None, ): if identity_api_version is None: identity_api_version = '2.0' if auth_plugin_name is None: auth_plugin_name = 'password' if auth_plugin_name.endswith('password'): auth_dict = copy.deepcopy(self.default_password_auth) elif auth_plugin_name.endswith('token'): auth_dict = copy.deepcopy(self.default_token_auth) else: auth_dict = {} if auth_args is not None: auth_dict = auth_args cli_options = defaults.get_defaults() cli_options.update({ 'auth_type': auth_plugin_name, 'auth': auth_dict, 'interface': fakes.INTERFACE, 'region_name': fakes.REGION_NAME, # 'workflow_api_version': '2', }) if config_args is not None: cli_options.update(config_args) loader = loading.get_plugin_loader(auth_plugin_name) auth_plugin = loader.load_from_options(**auth_dict) client_manager = self._clientmanager_class()( cli_options=cloud_region.CloudRegion( name='t1', region_name='1', config=cli_options, auth_plugin=auth_plugin, ), api_version={ 'identity': identity_api_version, }, ) client_manager._auth_required = auth_required is True client_manager.setup_auth() client_manager.auth_ref self.assertEqual( auth_plugin_name, client_manager.auth_plugin_name, ) return client_manager class TestShell(TestCase): # Full name of the OpenStackShell class to test (cliff.app.App subclass) shell_class_name = "osc_lib.shell.OpenStackShell" def setUp(self): super(TestShell, self).setUp() self.shell_class = importutils.import_class(self.shell_class_name) self.cmd_patch = mock.patch(self.shell_class_name + ".run_subcommand") self.cmd_save = self.cmd_patch.start() self.addCleanup(self.cmd_patch.stop) self.app = mock.Mock("Test Shell") def _assert_initialize_app_arg(self, cmd_options, default_args): """Check the args passed to initialize_app() The argv argument to initialize_app() is the remainder from parsing global options declared in both cliff.app and osc_lib.OpenStackShell build_option_parser(). Any global options passed on the command line should not be in argv but in _shell.options. """ with mock.patch( self.shell_class_name + ".initialize_app", self.app, ): _shell = make_shell(shell_class=self.shell_class) _cmd = cmd_options + " module list" fake_execute(_shell, _cmd) self.app.assert_called_with(["module", "list"]) for k in default_args.keys(): self.assertEqual( default_args[k], vars(_shell.options)[k], "%s does not match" % k, ) def _assert_cloud_region_arg(self, cmd_options, default_args): """Check the args passed to OpenStackConfig.get_one() The argparse argument to get_one() is an argparse.Namespace object that contains all of the options processed to this point in initialize_app(). """ cloud = mock.Mock(name="cloudy") cloud.config = {} self.occ_get_one = mock.Mock(return_value=cloud) with mock.patch( "openstack.config.loader.OpenStackConfig.get_one", self.occ_get_one, ): _shell = make_shell(shell_class=self.shell_class) _cmd = cmd_options + " module list" fake_execute(_shell, _cmd) self.app.assert_called_with(["module", "list"]) opts = self.occ_get_one.call_args[1]['argparse'] for k in default_args.keys(): self.assertEqual( default_args[k], vars(opts)[k], "%s does not match" % k, ) def _test_options_init_app(self, test_opts): """Test options on the command line""" for opt in test_opts.keys(): if not test_opts[opt][1]: continue key = opt2attr(opt) if isinstance(test_opts[opt][0], str): cmd = opt + " " + test_opts[opt][0] else: cmd = opt kwargs = { key: test_opts[opt][0], } self._assert_initialize_app_arg(cmd, kwargs) def _test_env_init_app(self, test_opts): """Test options in the environment""" for opt in test_opts.keys(): if not test_opts[opt][2]: continue key = opt2attr(opt) kwargs = { key: test_opts[opt][0], } env = { opt2env(opt): test_opts[opt][0], } os.environ = env.copy() self._assert_initialize_app_arg("", kwargs) def _test_options_get_one_cloud(self, test_opts): """Test options sent "to openstack.config""" for opt in test_opts.keys(): if not test_opts[opt][1]: continue key = opt2attr(opt) if isinstance(test_opts[opt][0], str): cmd = opt + " " + test_opts[opt][0] else: cmd = opt kwargs = { key: test_opts[opt][0], } self._assert_cloud_region_arg(cmd, kwargs) def _test_env_get_one_cloud(self, test_opts): """Test environment options sent "to openstack.config""" for opt in test_opts.keys(): if not test_opts[opt][2]: continue key = opt2attr(opt) kwargs = { key: test_opts[opt][0], } env = { opt2env(opt): test_opts[opt][0], } os.environ = env.copy() self._assert_cloud_region_arg("", kwargs) osc-lib-2.0.0/osc_lib/tests/utils/test_utils.py0000664000175000017500000010424313607371235021563 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import six import time import uuid from cliff import columns as cliff_columns import mock from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib.tests import fakes from osc_lib.tests import utils as test_utils from osc_lib import utils PASSWORD = "Pa$$w0rd" WASSPORD = "Wa$$p0rd" DROWSSAP = "dr0w$$aP" class FakeOddballResource(fakes.FakeResource): def get(self, attr): """get() is needed for utils.find_resource()""" if attr == 'id': return self.id elif attr == 'name': return self.name else: return None class TestUtils(test_utils.TestCase): def _get_test_items(self): item1 = {'a': 1, 'b': 2} item2 = {'a': 1, 'b': 3} item3 = {'a': 2, 'b': 2} item4 = {'a': 2, 'b': 1} return [item1, item2, item3, item4] def test_find_min_match_no_sort(self): items = self._get_test_items() sort_str = None flair = {} expect_items = items self.assertEqual( expect_items, list(utils.find_min_match(items, sort_str, **flair)), ) def test_find_min_match_no_flair(self): items = self._get_test_items() sort_str = 'b' flair = {} expect_items = [items[3], items[0], items[2], items[1]] self.assertEqual( expect_items, utils.find_min_match(items, sort_str, **flair), ) def test_find_min_match_a2(self): items = self._get_test_items() sort_str = 'b' flair = {'a': 2} expect_items = [items[3], items[2]] self.assertEqual( expect_items, utils.find_min_match(items, sort_str, **flair), ) def test_find_min_match_b2(self): items = self._get_test_items() sort_str = 'b' flair = {'b': 2} expect_items = [items[0], items[2], items[1]] self.assertEqual( expect_items, utils.find_min_match(items, sort_str, **flair), ) def test_find_min_match_b5(self): items = self._get_test_items() sort_str = 'b' flair = {'b': 5} expect_items = [] self.assertEqual( expect_items, utils.find_min_match(items, sort_str, **flair), ) def test_find_min_match_a2_b2(self): items = self._get_test_items() sort_str = 'b' flair = {'a': 2, 'b': 2} expect_items = [items[2]] self.assertEqual( expect_items, utils.find_min_match(items, sort_str, **flair), ) def test_get_password_good(self): with mock.patch("getpass.getpass", return_value=PASSWORD): mock_stdin = mock.Mock() mock_stdin.isatty = mock.Mock() mock_stdin.isatty.return_value = True self.assertEqual(PASSWORD, utils.get_password(mock_stdin)) def test_get_password_bad_once(self): answers = [PASSWORD, WASSPORD, DROWSSAP, DROWSSAP] with mock.patch("getpass.getpass", side_effect=answers): mock_stdin = mock.Mock() mock_stdin.isatty = mock.Mock() mock_stdin.isatty.return_value = True self.assertEqual(DROWSSAP, utils.get_password(mock_stdin)) def test_get_password_no_tty(self): mock_stdin = mock.Mock() mock_stdin.isatty = mock.Mock() mock_stdin.isatty.return_value = False self.assertRaises(exceptions.CommandError, utils.get_password, mock_stdin) def test_get_password_cntrl_d(self): with mock.patch("getpass.getpass", side_effect=EOFError()): mock_stdin = mock.Mock() mock_stdin.isatty = mock.Mock() mock_stdin.isatty.return_value = True self.assertRaises(exceptions.CommandError, utils.get_password, mock_stdin) def test_sort_items_with_one_key(self): items = self._get_test_items() sort_str = 'b' expect_items = [items[3], items[0], items[2], items[1]] self.assertEqual(expect_items, utils.sort_items(items, sort_str)) def test_sort_items_with_multiple_keys(self): items = self._get_test_items() sort_str = 'a,b' expect_items = [items[0], items[1], items[3], items[2]] self.assertEqual(expect_items, utils.sort_items(items, sort_str)) def test_sort_items_all_with_direction(self): items = self._get_test_items() sort_str = 'a:desc,b:desc' expect_items = [items[2], items[3], items[1], items[0]] self.assertEqual(expect_items, utils.sort_items(items, sort_str)) def test_sort_items_some_with_direction(self): items = self._get_test_items() sort_str = 'a,b:desc' expect_items = [items[1], items[0], items[2], items[3]] self.assertEqual(expect_items, utils.sort_items(items, sort_str)) def test_sort_items_with_object(self): item1 = mock.Mock(a=1, b=2) item2 = mock.Mock(a=1, b=3) item3 = mock.Mock(a=2, b=2) item4 = mock.Mock(a=2, b=1) items = [item1, item2, item3, item4] sort_str = 'b,a' expect_items = [item4, item1, item3, item2] self.assertEqual(expect_items, utils.sort_items(items, sort_str)) def test_sort_items_with_empty_key(self): items = self._get_test_items() sort_srt = '' self.assertEqual(items, utils.sort_items(items, sort_srt)) sort_srt = None self.assertEqual(items, utils.sort_items(items, sort_srt)) def test_sort_items_with_invalid_key(self): items = self._get_test_items() sort_str = 'c' self.assertRaises(exceptions.CommandError, utils.sort_items, items, sort_str) def test_sort_items_with_invalid_direction(self): items = self._get_test_items() sort_str = 'a:bad_dir' self.assertRaises(exceptions.CommandError, utils.sort_items, items, sort_str) def test_sort_items_with_different_type_exception(self): item1 = {'a': 2} item2 = {'a': 3} item3 = {'a': None} item4 = {'a': 1} items = [item1, item2, item3, item4] sort_str = 'a' expect_items = [item3, item4, item1, item2] if six.PY2: self.assertEqual(expect_items, utils.sort_items(items, sort_str)) else: self.assertRaises(TypeError, utils.sort_items, items, sort_str) def test_sort_items_with_different_type_int(self): item1 = {'a': 2} item2 = {'a': 3} item3 = {'a': None} item4 = {'a': 1} items = [item1, item2, item3, item4] sort_str = 'a' sort_type = int expect_items = [item3, item4, item1, item2] self.assertEqual(expect_items, utils.sort_items(items, sort_str, sort_type)) def test_sort_items_with_different_type_str(self): item1 = {'a': 'a'} item2 = {'a': None} item3 = {'a': '2'} item4 = {'a': 'b'} items = [item1, item2, item3, item4] sort_str = 'a' sort_type = str expect_items = [item3, item2, item1, item4] self.assertEqual(expect_items, utils.sort_items(items, sort_str, sort_type)) @mock.patch.object(time, 'sleep') def test_wait_for_delete_ok(self, mock_sleep): # Tests the normal flow that the resource is deleted with a 404 coming # back on the 2nd iteration of the wait loop. resource = mock.MagicMock(status='ACTIVE', progress=None) mock_get = mock.Mock(side_effect=[resource, exceptions.NotFound(404)]) manager = mock.MagicMock(get=mock_get) res_id = str(uuid.uuid4()) callback = mock.Mock() self.assertTrue(utils.wait_for_delete(manager, res_id, callback=callback)) mock_sleep.assert_called_once_with(5) callback.assert_called_once_with(0) @mock.patch.object(time, 'sleep') def test_wait_for_delete_timeout(self, mock_sleep): # Tests that we fail if the resource is not deleted before the timeout. resource = mock.MagicMock(status='ACTIVE') mock_get = mock.Mock(return_value=resource) manager = mock.MagicMock(get=mock_get) res_id = str(uuid.uuid4()) self.assertFalse(utils.wait_for_delete(manager, res_id, sleep_time=1, timeout=1)) mock_sleep.assert_called_once_with(1) @mock.patch.object(time, 'sleep') def test_wait_for_delete_error(self, mock_sleep): # Tests that we fail if the resource goes to error state while waiting. resource = mock.MagicMock(status='ERROR') mock_get = mock.Mock(return_value=resource) manager = mock.MagicMock(get=mock_get) res_id = str(uuid.uuid4()) self.assertFalse(utils.wait_for_delete(manager, res_id)) mock_sleep.assert_not_called() @mock.patch.object(time, 'sleep') def test_wait_for_delete_error_with_overrides(self, mock_sleep): # Tests that we fail if the resource is my_status=failed resource = mock.MagicMock(my_status='FAILED') mock_get = mock.Mock(return_value=resource) manager = mock.MagicMock(get=mock_get) res_id = str(uuid.uuid4()) self.assertFalse(utils.wait_for_delete(manager, res_id, status_field='my_status', error_status=['failed'])) mock_sleep.assert_not_called() @mock.patch.object(time, 'sleep') def test_wait_for_delete_error_with_overrides_exception(self, mock_sleep): # Tests that we succeed if the resource is specific exception mock_get = mock.Mock(side_effect=Exception) manager = mock.MagicMock(get=mock_get) res_id = str(uuid.uuid4()) self.assertTrue(utils.wait_for_delete(manager, res_id, exception_name=['Exception'])) mock_sleep.assert_not_called() @mock.patch.object(time, 'sleep') def test_wait_for_status_ok(self, mock_sleep): # Tests the normal flow that the resource is status=active resource = mock.MagicMock(status='ACTIVE') status_f = mock.Mock(return_value=resource) res_id = str(uuid.uuid4()) self.assertTrue(utils.wait_for_status(status_f, res_id,)) mock_sleep.assert_not_called() @mock.patch.object(time, 'sleep') def test_wait_for_status_ok_with_overrides(self, mock_sleep): # Tests the normal flow that the resource is status=complete resource = mock.MagicMock(my_status='COMPLETE') status_f = mock.Mock(return_value=resource) res_id = str(uuid.uuid4()) self.assertTrue(utils.wait_for_status(status_f, res_id, status_field='my_status', success_status=['complete'])) mock_sleep.assert_not_called() @mock.patch.object(time, 'sleep') def test_wait_for_status_error(self, mock_sleep): # Tests that we fail if the resource is status=error resource = mock.MagicMock(status='ERROR') status_f = mock.Mock(return_value=resource) res_id = str(uuid.uuid4()) self.assertFalse(utils.wait_for_status(status_f, res_id)) mock_sleep.assert_not_called() @mock.patch.object(time, 'sleep') def test_wait_for_status_error_with_overrides(self, mock_sleep): # Tests that we fail if the resource is my_status=failed resource = mock.MagicMock(my_status='FAILED') status_f = mock.Mock(return_value=resource) res_id = str(uuid.uuid4()) self.assertFalse(utils.wait_for_status(status_f, res_id, status_field='my_status', error_status=['failed'])) mock_sleep.assert_not_called() def test_build_kwargs_dict_value_set(self): self.assertEqual({'arg_bla': 'bla'}, utils.build_kwargs_dict('arg_bla', 'bla')) def test_build_kwargs_dict_value_None(self): self.assertEqual({}, utils.build_kwargs_dict('arg_bla', None)) def test_build_kwargs_dict_value_empty_str(self): self.assertEqual({}, utils.build_kwargs_dict('arg_bla', '')) def test_is_ascii_bytes(self): self.assertFalse(utils.is_ascii(b'\xe2')) def test_is_ascii_string(self): self.assertFalse(utils.is_ascii(u'\u2665')) def test_format_size(self): self.assertEqual("999", utils.format_size(999)) self.assertEqual("100K", utils.format_size(100000)) self.assertEqual("2M", utils.format_size(2000000)) self.assertEqual( "16.4M", utils.format_size(16361280) ) self.assertEqual( "1.6G", utils.format_size(1576395005) ) self.assertEqual("0", utils.format_size(None)) def test_backward_compat_col_lister(self): fake_col_headers = ['ID', 'Name', 'Size'] columns = ['Display Name'] column_map = {'Display Name': 'Name'} results = utils.backward_compat_col_lister(fake_col_headers, columns, column_map) self.assertIsInstance(results, list) self.assertIn('Display Name', results) self.assertNotIn('Name', results) self.assertIn('ID', results) self.assertIn('Size', results) def test_backward_compat_col_lister_no_specify_column(self): fake_col_headers = ['ID', 'Name', 'Size'] columns = [] column_map = {'Display Name': 'Name'} results = utils.backward_compat_col_lister(fake_col_headers, columns, column_map) self.assertIsInstance(results, list) self.assertNotIn('Display Name', results) self.assertIn('Name', results) self.assertIn('ID', results) self.assertIn('Size', results) def test_backward_compat_col_lister_with_tuple_headers(self): fake_col_headers = ('ID', 'Name', 'Size') columns = ['Display Name'] column_map = {'Display Name': 'Name'} results = utils.backward_compat_col_lister(fake_col_headers, columns, column_map) self.assertIsInstance(results, list) self.assertIn('Display Name', results) self.assertNotIn('Name', results) self.assertIn('ID', results) self.assertIn('Size', results) def test_backward_compat_col_showone(self): fake_object = {'id': 'fake-id', 'name': 'fake-name', 'size': 'fake-size'} columns = ['display_name'] column_map = {'display_name': 'name'} results = utils.backward_compat_col_showone(fake_object, columns, column_map) self.assertIsInstance(results, dict) self.assertIn('display_name', results) self.assertIn('id', results) self.assertNotIn('name', results) self.assertIn('size', results) def test_backward_compat_col_showone_no_specify_column(self): fake_object = {'id': 'fake-id', 'name': 'fake-name', 'size': 'fake-size'} columns = [] column_map = {'display_name': 'name'} results = utils.backward_compat_col_showone(fake_object, columns, column_map) self.assertIsInstance(results, dict) self.assertNotIn('display_name', results) self.assertIn('id', results) self.assertIn('name', results) self.assertIn('size', results) def _test_get_item_properties_with_formatter(self, formatters): names = ('id', 'attr') item = fakes.FakeResource(info={'id': 'fake-id', 'attr': ['a', 'b']}) res_id, res_attr = utils.get_item_properties(item, names, formatters=formatters) self.assertEqual('fake-id', res_id) return res_attr def test_get_item_properties_with_format_func(self): formatters = {'attr': utils.format_list} res_attr = self._test_get_item_properties_with_formatter(formatters) self.assertEqual(utils.format_list(['a', 'b']), res_attr) def test_get_item_properties_with_formattable_column(self): formatters = {'attr': format_columns.ListColumn} res_attr = self._test_get_item_properties_with_formatter(formatters) self.assertIsInstance(res_attr, format_columns.ListColumn) def _test_get_dict_properties_with_formatter(self, formatters): names = ('id', 'attr') item = {'id': 'fake-id', 'attr': ['a', 'b']} res_id, res_attr = utils.get_dict_properties(item, names, formatters=formatters) self.assertEqual('fake-id', res_id) return res_attr def test_get_dict_properties_with_format_func(self): formatters = {'attr': utils.format_list} res_attr = self._test_get_dict_properties_with_formatter(formatters) self.assertEqual(utils.format_list(['a', 'b']), res_attr) def test_get_dict_properties_with_formattable_column(self): formatters = {'attr': format_columns.ListColumn} res_attr = self._test_get_dict_properties_with_formatter(formatters) self.assertIsInstance(res_attr, format_columns.ListColumn) def _test_calculate_header_and_attrs(self, parsed_args_columns, expected_headers, expected_attrs): column_headers = ('ID', 'Name', 'Fixed IP Addresses') columns = ('id', 'name', 'fixed_ips') parsed_args = mock.Mock() parsed_args.columns = parsed_args_columns ret_headers, ret_attrs = utils.calculate_header_and_attrs( column_headers, columns, parsed_args) self.assertEqual(expected_headers, ret_headers) self.assertEqual(expected_attrs, ret_attrs) if parsed_args_columns: self.assertEqual(expected_headers, parsed_args.columns) else: self.assertFalse(parsed_args.columns) def test_calculate_header_and_attrs_without_column_arg(self): self._test_calculate_header_and_attrs( [], ('ID', 'Name', 'Fixed IP Addresses'), ('id', 'name', 'fixed_ips')) def test_calculate_header_and_attrs_with_known_columns(self): self._test_calculate_header_and_attrs( ['Name', 'ID'], ['Name', 'ID'], ['name', 'id']) def test_calculate_header_and_attrs_with_unknown_columns(self): self._test_calculate_header_and_attrs( ['Name', 'ID', 'device_id'], ['Name', 'ID', 'device_id'], ['name', 'id', 'device_id']) def test_calculate_header_and_attrs_with_attrname_columns(self): self._test_calculate_header_and_attrs( ['name', 'id', 'device_id'], ['Name', 'ID', 'device_id'], ['name', 'id', 'device_id']) def test_subtest(self): for i in range(3): with self.subTest(i=i): self.assertEqual(i, i) class NoUniqueMatch(Exception): pass class TestFindResource(test_utils.TestCase): def setUp(self): super(TestFindResource, self).setUp() self.name = 'legos' self.expected = mock.Mock() self.manager = mock.Mock() self.manager.resource_class = mock.Mock() self.manager.resource_class.__name__ = 'lego' def test_find_resource_get_int(self): self.manager.get = mock.Mock(return_value=self.expected) result = utils.find_resource(self.manager, 1) self.assertEqual(self.expected, result) self.manager.get.assert_called_with(1) def test_find_resource_get_int_string(self): self.manager.get = mock.Mock(return_value=self.expected) result = utils.find_resource(self.manager, "2") self.assertEqual(self.expected, result) self.manager.get.assert_called_with("2") def test_find_resource_get_name_and_domain(self): name = 'admin' domain_id = '30524568d64447fbb3fa8b7891c10dd6' # NOTE(stevemar): we need an iterable side-effect because the same # function (manager.get()) is used twice, the first time an exception # will happen, then the result will be found, but only after using # the domain ID as a query arg side_effect = [Exception('Boom!'), self.expected] self.manager.get = mock.Mock(side_effect=side_effect) result = utils.find_resource(self.manager, name, domain_id=domain_id) self.assertEqual(self.expected, result) self.manager.get.assert_called_with(name, domain_id=domain_id) def test_find_resource_get_uuid(self): uuid = '9a0dc2a0-ad0d-11e3-a5e2-0800200c9a66' self.manager.get = mock.Mock(return_value=self.expected) result = utils.find_resource(self.manager, uuid) self.assertEqual(self.expected, result) self.manager.get.assert_called_with(uuid) def test_find_resource_get_whatever(self): self.manager.get = mock.Mock(return_value=self.expected) result = utils.find_resource(self.manager, 'whatever') self.assertEqual(self.expected, result) self.manager.get.assert_called_with('whatever') def test_find_resource_find(self): self.manager.get = mock.Mock(side_effect=Exception('Boom!')) self.manager.find = mock.Mock(return_value=self.expected) result = utils.find_resource(self.manager, self.name) self.assertEqual(self.expected, result) self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) def test_find_resource_find_not_found(self): self.manager.get = mock.Mock(side_effect=Exception('Boom!')) self.manager.find = mock.Mock( side_effect=exceptions.NotFound(404, "2") ) result = self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, self.name) self.assertEqual("No lego with a name or ID of 'legos' exists.", str(result)) self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) def test_find_resource_list_forbidden(self): self.manager.get = mock.Mock(side_effect=Exception('Boom!')) self.manager.find = mock.Mock(side_effect=Exception('Boom!')) self.manager.list = mock.Mock( side_effect=exceptions.Forbidden(403) ) self.assertRaises(exceptions.Forbidden, utils.find_resource, self.manager, self.name) self.manager.list.assert_called_with() def test_find_resource_find_no_unique(self): self.manager.get = mock.Mock(side_effect=Exception('Boom!')) self.manager.find = mock.Mock(side_effect=NoUniqueMatch()) result = self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, self.name) self.assertEqual("More than one lego exists with the name 'legos'.", str(result)) self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) def test_find_resource_silly_resource(self): # We need a resource with no resource_class for this test, start fresh self.manager = mock.Mock() self.manager.get = mock.Mock(side_effect=Exception('Boom!')) self.manager.find = mock.Mock( side_effect=AttributeError( "'Controller' object has no attribute 'find'", ) ) silly_resource = FakeOddballResource( None, {'id': '12345', 'name': self.name}, loaded=True, ) self.manager.list = mock.Mock( return_value=[silly_resource, ], ) result = utils.find_resource(self.manager, self.name) self.assertEqual(silly_resource, result) self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) def test_find_resource_silly_resource_not_found(self): # We need a resource with no resource_class for this test, start fresh self.manager = mock.Mock() self.manager.get = mock.Mock(side_effect=Exception('Boom!')) self.manager.find = mock.Mock( side_effect=AttributeError( "'Controller' object has no attribute 'find'", ) ) self.manager.list = mock.Mock(return_value=[]) result = self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, self.name) self.assertEqual("Could not find resource legos", str(result)) self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) def test_find_resource_silly_resource_no_unique_match(self): # We need a resource with no resource_class for this test, start fresh self.manager = mock.Mock() self.manager.get = mock.Mock(side_effect=Exception('Boom!')) self.manager.find = mock.Mock( side_effect=AttributeError( "'Controller' object has no attribute 'find'", ) ) silly_resource = FakeOddballResource( None, {'id': '12345', 'name': self.name}, loaded=True, ) silly_resource_same = FakeOddballResource( None, {'id': 'abcde', 'name': self.name}, loaded=True, ) self.manager.list = mock.Mock(return_value=[silly_resource, silly_resource_same]) result = self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, self.name) self.assertEqual("More than one resource exists " "with the name or ID 'legos'.", str(result)) self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) def test_format_dict(self): expected = "a='b', c='d', e='f'" self.assertEqual(expected, utils.format_dict({'a': 'b', 'c': 'd', 'e': 'f'})) self.assertEqual(expected, utils.format_dict({'e': 'f', 'c': 'd', 'a': 'b'})) self.assertIsNone(utils.format_dict(None)) def test_format_dict_recursive(self): expected = "a='b', c.1='d', c.2=''" self.assertEqual( expected, utils.format_dict({'a': 'b', 'c': {'1': 'd', '2': ''}}) ) self.assertEqual( expected, utils.format_dict({'c': {'1': 'd', '2': ''}, 'a': 'b'}) ) self.assertIsNone(utils.format_dict(None)) expected = "a1='A', a2.b1.c1='B', a2.b1.c2=, a2.b2='D'" self.assertEqual( expected, utils.format_dict( { 'a1': 'A', 'a2': { 'b1': { 'c1': 'B', 'c2': None, }, 'b2': 'D', }, } ) ) self.assertEqual( expected, utils.format_dict( { 'a2': { 'b1': { 'c2': None, 'c1': 'B', }, 'b2': 'D', }, 'a1': 'A', } ) ) def test_format_dict_of_list(self): expected = "a=a1, a2; b=b1, b2; c=c1, c2; e=" self.assertEqual(expected, utils.format_dict_of_list({'a': ['a2', 'a1'], 'b': ['b2', 'b1'], 'c': ['c1', 'c2'], 'd': None, 'e': []}) ) self.assertEqual(expected, utils.format_dict_of_list({'c': ['c1', 'c2'], 'a': ['a2', 'a1'], 'b': ['b2', 'b1'], 'e': []}) ) self.assertIsNone(utils.format_dict_of_list(None)) def test_format_dict_of_list_with_separator(self): expected = "a=a1, a2\nb=b1, b2\nc=c1, c2\ne=" self.assertEqual(expected, utils.format_dict_of_list({'a': ['a2', 'a1'], 'b': ['b2', 'b1'], 'c': ['c1', 'c2'], 'd': None, 'e': []}, separator='\n') ) self.assertEqual(expected, utils.format_dict_of_list({'c': ['c1', 'c2'], 'a': ['a2', 'a1'], 'b': ['b2', 'b1'], 'e': []}, separator='\n') ) self.assertIsNone(utils.format_dict_of_list(None, separator='\n')) def test_format_list(self): expected = 'a, b, c' self.assertEqual(expected, utils.format_list(['a', 'b', 'c'])) self.assertEqual(expected, utils.format_list(['c', 'b', 'a'])) self.assertIsNone(utils.format_list(None)) def test_format_list_of_dicts(self): expected = "a='b', c='d'\ne='f'" sorted_data = [{'a': 'b', 'c': 'd'}, {'e': 'f'}] unsorted_data = [{'c': 'd', 'a': 'b'}, {'e': 'f'}] self.assertEqual(expected, utils.format_list_of_dicts(sorted_data)) self.assertEqual(expected, utils.format_list_of_dicts(unsorted_data)) self.assertEqual('', utils.format_list_of_dicts([])) self.assertEqual('', utils.format_list_of_dicts([{}])) self.assertIsNone(utils.format_list_of_dicts(None)) def test_format_list_separator(self): expected = 'a\nb\nc' actual_pre_sorted = utils.format_list(['a', 'b', 'c'], separator='\n') actual_unsorted = utils.format_list(['c', 'b', 'a'], separator='\n') self.assertEqual(expected, actual_pre_sorted) self.assertEqual(expected, actual_unsorted) class TestAssertItemEqual(test_utils.TestCommand): def test_assert_normal_item(self): expected = ['a', 'b', 'c'] actual = ['a', 'b', 'c'] self.assertItemEqual(expected, actual) def test_assert_item_with_formattable_columns(self): expected = [format_columns.DictColumn({'a': 1, 'b': 2}), format_columns.ListColumn(['x', 'y', 'z'])] actual = [format_columns.DictColumn({'a': 1, 'b': 2}), format_columns.ListColumn(['x', 'y', 'z'])] self.assertItemEqual(expected, actual) def test_assert_item_different_length(self): expected = ['a', 'b', 'c'] actual = ['a', 'b'] self.assertRaises(AssertionError, self.assertItemEqual, expected, actual) def test_assert_item_formattable_columns_vs_legacy_formatter(self): expected = [format_columns.DictColumn({'a': 1, 'b': 2}), format_columns.ListColumn(['x', 'y', 'z'])] actual = [utils.format_dict({'a': 1, 'b': 2}), utils.format_list(['x', 'y', 'z'])] self.assertRaises(AssertionError, self.assertItemEqual, expected, actual) def test_assert_item_different_formattable_columns(self): class ExceptionColumn(cliff_columns.FormattableColumn): def human_readable(self): raise Exception('always fail') expected = [format_columns.DictColumn({'a': 1, 'b': 2})] actual = [ExceptionColumn({'a': 1, 'b': 2})] # AssertionError is a subclass of Exception # so raising AssertionError ensures ExceptionColumn.human_readable() # is not called. self.assertRaises(AssertionError, self.assertItemEqual, expected, actual) def test_assert_list_item(self): expected = [ ['a', 'b', 'c'], [format_columns.DictColumn({'a': 1, 'b': 2}), format_columns.ListColumn(['x', 'y', 'z'])] ] actual = [ ['a', 'b', 'c'], [format_columns.DictColumn({'a': 1, 'b': 2}), format_columns.ListColumn(['x', 'y', 'z'])] ] self.assertListItemEqual(expected, actual) osc-lib-2.0.0/osc_lib/tests/test_clientmanager.py0000664000175000017500000003571413607371235022102 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import copy import mock from keystoneauth1.access import service_catalog from keystoneauth1 import exceptions as ksa_exceptions from keystoneauth1.identity import generic as generic_plugin from keystoneauth1.identity.v3 import k2k from keystoneauth1 import loading from keystoneauth1 import noauth from keystoneauth1 import token_endpoint from openstack.config import cloud_config from openstack.config import defaults from openstack import connection from osc_lib.api import auth from osc_lib import clientmanager from osc_lib import exceptions as exc from osc_lib.tests import fakes from osc_lib.tests import utils AUTH_REF = {'version': 'v2.0'} AUTH_REF.update(fakes.TEST_RESPONSE_DICT['access']) SERVICE_CATALOG = service_catalog.ServiceCatalogV2(AUTH_REF) AUTH_DICT = { 'auth_url': fakes.AUTH_URL, 'username': fakes.USERNAME, 'password': fakes.PASSWORD, 'project_name': fakes.PROJECT_NAME } # This is deferred in api.auth but we need it here... auth.get_options_list() class Container(object): attr = clientmanager.ClientCache(lambda x: object()) buggy_attr = clientmanager.ClientCache(lambda x: x.foo) def __init__(self): pass class TestClientCache(utils.TestCase): def test_singleton(self): # NOTE(dtroyer): Verify that the ClientCache descriptor only invokes # the factory one time and always returns the same value after that. c = Container() self.assertEqual(c.attr, c.attr) def test_attribute_error_propagates(self): c = Container() err = self.assertRaises(exc.PluginAttributeError, getattr, c, 'buggy_attr') self.assertNotIsInstance(err, AttributeError) self.assertEqual("'Container' object has no attribute 'foo'", str(err)) class TestClientManager(utils.TestClientManager): def test_client_manager_none(self): none_auth = { 'endpoint': fakes.AUTH_URL, } client_manager = self._make_clientmanager( auth_args=none_auth, auth_plugin_name='none', ) self.assertEqual( fakes.AUTH_URL, client_manager._cli_options.config['auth']['endpoint'], ) self.assertIsInstance( client_manager.auth, noauth.NoAuth, ) # Check that the endpoint option works as the override self.assertEqual( fakes.AUTH_URL, client_manager.get_endpoint_for_service_type('baremetal'), ) def test_client_manager_admin_token(self): token_auth = { 'endpoint': fakes.AUTH_URL, 'token': fakes.AUTH_TOKEN, } client_manager = self._make_clientmanager( auth_args=token_auth, auth_plugin_name='admin_token', ) self.assertEqual( fakes.AUTH_URL, client_manager._cli_options.config['auth']['endpoint'], ) self.assertEqual( fakes.AUTH_TOKEN, client_manager.auth.get_token(None), ) self.assertIsInstance( client_manager.auth, token_endpoint.Token, ) # NOTE(dtroyer): This is intentionally not assertFalse() as the return # value from is_service_available() may be == None self.assertNotEqual( False, client_manager.is_service_available('network'), ) def test_client_manager_password(self): client_manager = self._make_clientmanager( auth_required=True, ) self.assertEqual( fakes.AUTH_URL, client_manager._cli_options.config['auth']['auth_url'], ) self.assertEqual( fakes.USERNAME, client_manager._cli_options.config['auth']['username'], ) self.assertEqual( fakes.PASSWORD, client_manager._cli_options.config['auth']['password'], ) self.assertIsInstance( client_manager.auth, generic_plugin.Password, ) self.assertTrue(client_manager.verify) self.assertIsNone(client_manager.cert) # These need to stick around until the old-style clients are gone self.assertEqual( AUTH_REF.pop('version'), client_manager.auth_ref.version, ) self.assertEqual( fakes.to_unicode_dict(AUTH_REF), client_manager.auth_ref._data['access'], ) self.assertEqual( dir(SERVICE_CATALOG), dir(client_manager.auth_ref.service_catalog), ) self.assertTrue(client_manager.is_service_available('network')) def test_client_manager_password_verify(self): client_manager = self._make_clientmanager( auth_required=True, ) self.assertTrue(client_manager.verify) self.assertIsNone(client_manager.cacert) self.assertTrue(client_manager.is_service_available('network')) def test_client_manager_password_verify_ca(self): config_args = { 'cacert': 'cafile', } client_manager = self._make_clientmanager( config_args=config_args, auth_required=True, ) # Test that client_manager.verify is Requests-compatible, # i.e. it contains the value of cafile here self.assertTrue(client_manager.verify) self.assertEqual('cafile', client_manager.verify) self.assertEqual('cafile', client_manager.cacert) self.assertTrue(client_manager.is_service_available('network')) def test_client_manager_password_verify_false(self): config_args = { 'verify': False, } client_manager = self._make_clientmanager( config_args=config_args, auth_required=True, ) self.assertFalse(client_manager.verify) self.assertIsNone(client_manager.cacert) self.assertTrue(client_manager.is_service_available('network')) def test_client_manager_password_verify_insecure(self): config_args = { 'insecure': True, } client_manager = self._make_clientmanager( config_args=config_args, auth_required=True, ) self.assertFalse(client_manager.verify) self.assertIsNone(client_manager.cacert) self.assertTrue(client_manager.is_service_available('network')) def test_client_manager_password_verify_insecure_ca(self): config_args = { 'insecure': True, 'cacert': 'cafile', } client_manager = self._make_clientmanager( config_args=config_args, auth_required=True, ) # insecure overrides cacert self.assertFalse(client_manager.verify) self.assertIsNone(client_manager.cacert) self.assertTrue(client_manager.is_service_available('network')) def test_client_manager_password_client_cert(self): config_args = { 'cert': 'cert', } client_manager = self._make_clientmanager( config_args=config_args, ) self.assertEqual('cert', client_manager.cert) def test_client_manager_password_client_key(self): config_args = { 'cert': 'cert', 'key': 'key', } client_manager = self._make_clientmanager( config_args=config_args, ) self.assertEqual(('cert', 'key'), client_manager.cert) def test_client_manager_select_auth_plugin_password(self): # test password auth auth_args = { 'auth_url': fakes.AUTH_URL, 'username': fakes.USERNAME, 'password': fakes.PASSWORD, 'tenant_name': fakes.PROJECT_NAME, } self._make_clientmanager( auth_args=auth_args, identity_api_version='2.0', auth_plugin_name='v2password', ) auth_args = copy.deepcopy(self.default_password_auth) auth_args.update({ 'user_domain_name': 'default', 'project_domain_name': 'default', }) self._make_clientmanager( auth_args=auth_args, identity_api_version='3', auth_plugin_name='v3password', ) # Use v2.0 auth args auth_args = { 'auth_url': fakes.AUTH_URL, 'username': fakes.USERNAME, 'password': fakes.PASSWORD, 'tenant_name': fakes.PROJECT_NAME, } self._make_clientmanager( auth_args=auth_args, identity_api_version='2.0', ) # Use v3 auth args auth_args = copy.deepcopy(self.default_password_auth) auth_args.update({ 'user_domain_name': 'default', 'project_domain_name': 'default', }) self._make_clientmanager( auth_args=auth_args, identity_api_version='3', ) auth_args = copy.deepcopy(self.default_password_auth) auth_args.pop('username') auth_args.update({ 'user_id': fakes.USER_ID, }) self._make_clientmanager( auth_args=auth_args, identity_api_version='3', ) def test_client_manager_select_auth_plugin_token(self): # test token auth self._make_clientmanager( # auth_args=auth_args, identity_api_version='2.0', auth_plugin_name='v2token', ) self._make_clientmanager( # auth_args=auth_args, identity_api_version='3', auth_plugin_name='v3token', ) self._make_clientmanager( # auth_args=auth_args, identity_api_version='x', auth_plugin_name='token', ) def test_client_manager_select_auth_plugin_failure(self): self.assertRaises( ksa_exceptions.NoMatchingPlugin, self._make_clientmanager, identity_api_version='3', auth_plugin_name='bad_plugin', ) @mock.patch('osc_lib.api.auth.check_valid_authentication_options') def test_client_manager_auth_setup_once(self, check_authn_options_func): loader = loading.get_plugin_loader('password') auth_plugin = loader.load_from_options(**AUTH_DICT) cli_options = defaults.get_defaults() cli_options.update({ 'auth_type': 'password', 'auth': AUTH_DICT, 'interface': fakes.INTERFACE, 'region_name': fakes.REGION_NAME, }) client_manager = self._clientmanager_class()( cli_options=cloud_config.CloudConfig( name='t1', region='1', config=cli_options, auth_plugin=auth_plugin, ), api_version={ 'identity': '2.0', }, ) self.assertFalse(client_manager._auth_setup_completed) client_manager.setup_auth() self.assertTrue(check_authn_options_func.called) self.assertTrue(client_manager._auth_setup_completed) # now make sure we don't do auth setup the second time around # by checking whether check_valid_auth_options() gets called again check_authn_options_func.reset_mock() client_manager.auth_ref check_authn_options_func.assert_not_called() def test_client_manager_endpoint_disabled(self): auth_args = copy.deepcopy(self.default_password_auth) auth_args.update({ 'user_domain_name': 'default', 'project_domain_name': 'default', }) # v3 fake doesn't have network endpoint client_manager = self._make_clientmanager( auth_args=auth_args, identity_api_version='3', auth_plugin_name='v3password', ) self.assertFalse(client_manager.is_service_available('network')) def test_client_manager_k2k_auth_setup(self): loader = loading.get_plugin_loader('password') auth_plugin = loader.load_from_options(**AUTH_DICT) cli_options = defaults.get_defaults() cli_options.update({ 'auth_type': 'password', 'auth': AUTH_DICT, 'interface': fakes.INTERFACE, 'region_name': fakes.REGION_NAME, 'service_provider': fakes.SERVICE_PROVIDER_ID, 'remote_project_id': fakes.PROJECT_ID }) client_manager = self._clientmanager_class()( cli_options=cloud_config.CloudConfig( name='t1', region='1', config=cli_options, auth_plugin=auth_plugin, ), api_version={ 'identity': '3', }, ) self.assertFalse(client_manager._auth_setup_completed) client_manager.setup_auth() # Note(knikolla): Make sure that the auth object is of the correct # type and that the service_provider is correctly set. self.assertIsInstance(client_manager.auth, k2k.Keystone2Keystone) self.assertEqual(client_manager.auth._sp_id, fakes.SERVICE_PROVIDER_ID) self.assertEqual(client_manager.auth.project_id, fakes.PROJECT_ID) self.assertTrue(client_manager._auth_setup_completed) def test_client_manager_none_auth(self): # test token auth client_manager = self._make_clientmanager( auth_args={}, auth_plugin_name='none', ) self.assertIsNone( client_manager.get_endpoint_for_service_type('compute')) def test_client_manager_endpoint_override(self): # test token auth client_manager = self._make_clientmanager( auth_args={}, config_args={'compute_endpoint_override': 'http://example.com', 'foo_bar_endpoint_override': 'http://example2.com'}, auth_plugin_name='none', ) self.assertEqual( 'http://example.com', client_manager.get_endpoint_for_service_type('compute')) self.assertEqual( 'http://example2.com', client_manager.get_endpoint_for_service_type('foo-bar')) self.assertTrue(client_manager.is_service_available('compute')) class TestClientManagerSDK(utils.TestClientManager): def test_client_manager_connection(self): client_manager = self._make_clientmanager( auth_required=True, ) self.assertIsInstance( client_manager.sdk_connection, connection.Connection, ) osc-lib-2.0.0/osc_lib/tests/cli/0000775000175000017500000000000013607371310016407 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/tests/cli/test_parseractions.py0000664000175000017500000003346013607371235022711 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import argparse from osc_lib.cli import parseractions from osc_lib.tests import utils class TestKeyValueAction(utils.TestCase): def setUp(self): super(TestKeyValueAction, self).setUp() self.parser = argparse.ArgumentParser() # Set up our typical usage self.parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, default={'green': '20%', 'format': '#rgb'}, help='Property to store for this volume ' '(repeat option to set multiple properties)', ) def test_good_values(self): results = self.parser.parse_args([ '--property', 'red=', '--property', 'green=100%', '--property', 'blue=50%', ]) actual = getattr(results, 'property', {}) # All should pass through unmolested expect = {'red': '', 'green': '100%', 'blue': '50%', 'format': '#rgb'} self.assertEqual(expect, actual) def test_error_values(self): data_list = [ ['--property', 'red', ], ['--property', '=', ], ['--property', '=red', ] ] for data in data_list: self.assertRaises(argparse.ArgumentTypeError, self.parser.parse_args, data) class TestKeyValueAppendAction(utils.TestCase): def setUp(self): super(TestKeyValueAppendAction, self).setUp() self.parser = argparse.ArgumentParser() # Set up our typical usage self.parser.add_argument( '--hint', metavar='', action=parseractions.KeyValueAppendAction, help='Arbitrary key/value pairs to be sent to the scheduler for ' 'custom use', ) def test_good_values(self): print(self.parser._get_optional_actions()) results = self.parser.parse_args([ '--hint', 'same_host=a0cf03a5-d921-4877-bb5c-86d26cf818e1', '--hint', 'same_host=8c19174f-4220-44f0-824a-cd1eeef10287', '--hint', 'query=[>=,$free_ram_mb,1024]', ]) actual = getattr(results, 'hint', {}) expect = { 'same_host': [ 'a0cf03a5-d921-4877-bb5c-86d26cf818e1', '8c19174f-4220-44f0-824a-cd1eeef10287', ], 'query': [ '[>=,$free_ram_mb,1024]', ], } self.assertEqual(expect, actual) def test_error_values(self): data_list = [ ['--hint', 'red', ], ['--hint', '=', ], ['--hint', '=red', ] ] for data in data_list: self.assertRaises(argparse.ArgumentTypeError, self.parser.parse_args, data) class TestMultiKeyValueAction(utils.TestCase): def setUp(self): super(TestMultiKeyValueAction, self).setUp() self.parser = argparse.ArgumentParser() # Set up our typical usage self.parser.add_argument( '--test', metavar='req1=xxx,req2=yyy', action=parseractions.MultiKeyValueAction, dest='test', default=None, required_keys=['req1', 'req2'], optional_keys=['opt1', 'opt2'], help='Test' ) def test_good_values(self): results = self.parser.parse_args([ '--test', 'req1=aaa,req2=bbb', '--test', 'req1=,req2=', ]) actual = getattr(results, 'test', []) expect = [ {'req1': 'aaa', 'req2': 'bbb'}, {'req1': '', 'req2': ''}, ] self.assertItemsEqual(expect, actual) def test_empty_required_optional(self): self.parser.add_argument( '--test-empty', metavar='req1=xxx,req2=yyy', action=parseractions.MultiKeyValueAction, dest='test_empty', default=None, required_keys=[], optional_keys=[], help='Test' ) results = self.parser.parse_args([ '--test-empty', 'req1=aaa,req2=bbb', '--test-empty', 'req1=,req2=', ]) actual = getattr(results, 'test_empty', []) expect = [ {'req1': 'aaa', 'req2': 'bbb'}, {'req1': '', 'req2': ''}, ] self.assertItemsEqual(expect, actual) def test_error_values_with_comma(self): data_list = [ ['--test', 'mmm,nnn=zzz', ], ['--test', 'nnn=zzz,=', ], ['--test', 'nnn=zzz,=zzz', ] ] for data in data_list: self.assertRaises(argparse.ArgumentTypeError, self.parser.parse_args, data) def test_error_values_without_comma(self): self.assertRaises( argparse.ArgumentTypeError, self.parser.parse_args, [ '--test', 'mmmnnn', ] ) def test_missing_key(self): self.assertRaises( argparse.ArgumentTypeError, self.parser.parse_args, [ '--test', 'req2=ddd', ] ) def test_invalid_key(self): self.assertRaises( argparse.ArgumentTypeError, self.parser.parse_args, [ '--test', 'req1=aaa,req2=bbb,aaa=req1', ] ) def test_required_keys_not_list(self): self.assertRaises( TypeError, self.parser.add_argument, '--test-required-dict', metavar='req1=xxx,req2=yyy', action=parseractions.MultiKeyValueAction, dest='test_required_dict', default=None, required_keys={'aaa': 'bbb'}, optional_keys=['opt1', 'opt2'], help='Test' ) def test_optional_keys_not_list(self): self.assertRaises( TypeError, self.parser.add_argument, '--test-optional-dict', metavar='req1=xxx,req2=yyy', action=parseractions.MultiKeyValueAction, dest='test_optional_dict', default=None, required_keys=['req1', 'req2'], optional_keys={'aaa': 'bbb'}, help='Test' ) class TestMultiKeyValueCommaAction(utils.TestCase): def setUp(self): super(TestMultiKeyValueCommaAction, self).setUp() self.parser = argparse.ArgumentParser() # Typical usage self.parser.add_argument( '--test', metavar='req1=xxx,yyy', action=parseractions.MultiKeyValueCommaAction, dest='test', default=None, required_keys=['req1'], optional_keys=['opt2'], help='Test', ) def test_mkvca_required(self): results = self.parser.parse_args([ '--test', 'req1=aaa,bbb', ]) actual = getattr(results, 'test', []) expect = [ {'req1': 'aaa,bbb'}, ] self.assertItemsEqual(expect, actual) results = self.parser.parse_args([ '--test', 'req1=', ]) actual = getattr(results, 'test', []) expect = [ {'req1': ''}, ] self.assertItemsEqual(expect, actual) results = self.parser.parse_args([ '--test', 'req1=aaa,bbb', '--test', 'req1=', ]) actual = getattr(results, 'test', []) expect = [ {'req1': 'aaa,bbb'}, {'req1': ''}, ] self.assertItemsEqual(expect, actual) def test_mkvca_optional(self): results = self.parser.parse_args([ '--test', 'req1=aaa,bbb', ]) actual = getattr(results, 'test', []) expect = [ {'req1': 'aaa,bbb'}, ] self.assertItemsEqual(expect, actual) results = self.parser.parse_args([ '--test', 'req1=aaa,bbb', '--test', 'req1=,opt2=ccc', ]) actual = getattr(results, 'test', []) expect = [ {'req1': 'aaa,bbb'}, {'req1': '', 'opt2': 'ccc'}, ] self.assertItemsEqual(expect, actual) try: results = self.parser.parse_args([ '--test', 'req1=aaa,bbb', '--test', 'opt2=ccc', ]) self.fail('ArgumentTypeError should be raised') except argparse.ArgumentTypeError as e: self.assertEqual( 'Missing required keys req1.\nRequired keys are: req1', str(e), ) def test_mkvca_multiples(self): results = self.parser.parse_args([ '--test', 'req1=aaa,bbb,opt2=ccc', ]) actual = getattr(results, 'test', []) expect = [{ 'req1': 'aaa,bbb', 'opt2': 'ccc', }] self.assertItemsEqual(expect, actual) def test_mkvca_no_required_optional(self): self.parser.add_argument( '--test-empty', metavar='req1=xxx,yyy', action=parseractions.MultiKeyValueCommaAction, dest='test_empty', default=None, required_keys=[], optional_keys=[], help='Test', ) results = self.parser.parse_args([ '--test-empty', 'req1=aaa,bbb', ]) actual = getattr(results, 'test_empty', []) expect = [ {'req1': 'aaa,bbb'}, ] self.assertItemsEqual(expect, actual) results = self.parser.parse_args([ '--test-empty', 'xyz=aaa,bbb', ]) actual = getattr(results, 'test_empty', []) expect = [ {'xyz': 'aaa,bbb'}, ] self.assertItemsEqual(expect, actual) def test_mkvca_invalid_key(self): try: self.parser.parse_args([ '--test', 'req1=aaa,bbb=', ]) self.fail('ArgumentTypeError should be raised') except argparse.ArgumentTypeError as e: self.assertIn( 'Invalid keys bbb specified.\nValid keys are:', str(e), ) try: self.parser.parse_args([ '--test', 'nnn=aaa', ]) self.fail('ArgumentTypeError should be raised') except argparse.ArgumentTypeError as e: self.assertIn( 'Invalid keys nnn specified.\nValid keys are:', str(e), ) def test_mkvca_value_no_key(self): try: self.parser.parse_args([ '--test', 'req1=aaa,=bbb', ]) self.fail('ArgumentTypeError should be raised') except argparse.ArgumentTypeError as e: self.assertEqual( "A key must be specified before '=': =bbb", str(e), ) try: self.parser.parse_args([ '--test', '=nnn', ]) self.fail('ArgumentTypeError should be raised') except argparse.ArgumentTypeError as e: self.assertEqual( "A key must be specified before '=': =nnn", str(e), ) try: self.parser.parse_args([ '--test', 'nnn', ]) self.fail('ArgumentTypeError should be raised') except argparse.ArgumentTypeError as e: self.assertIn( 'A key=value pair is required:', str(e), ) def test_mkvca_required_keys_not_list(self): self.assertRaises( TypeError, self.parser.add_argument, '--test-required-dict', metavar='req1=xxx', action=parseractions.MultiKeyValueCommaAction, dest='test_required_dict', default=None, required_keys={'aaa': 'bbb'}, optional_keys=['opt1', 'opt2'], help='Test', ) def test_mkvca_optional_keys_not_list(self): self.assertRaises( TypeError, self.parser.add_argument, '--test-optional-dict', metavar='req1=xxx', action=parseractions.MultiKeyValueCommaAction, dest='test_optional_dict', default=None, required_keys=['req1', 'req2'], optional_keys={'aaa': 'bbb'}, help='Test', ) class TestNonNegativeAction(utils.TestCase): def setUp(self): super(TestNonNegativeAction, self).setUp() self.parser = argparse.ArgumentParser() # Set up our typical usage self.parser.add_argument( '--foo', metavar='', type=int, action=parseractions.NonNegativeAction, ) def test_negative_values(self): self.assertRaises( argparse.ArgumentTypeError, self.parser.parse_args, "--foo -1".split() ) def test_zero_values(self): results = self.parser.parse_args( '--foo 0'.split() ) actual = getattr(results, 'foo', None) self.assertEqual(actual, 0) def test_positive_values(self): results = self.parser.parse_args( '--foo 1'.split() ) actual = getattr(results, 'foo', None) self.assertEqual(actual, 1) osc-lib-2.0.0/osc_lib/tests/cli/test_identity.py0000664000175000017500000000643213607371235021664 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import mock from openstack import exceptions from openstack.identity.v3 import project import testtools from osc_lib.cli import identity as cli_identity from osc_lib.tests import utils as test_utils class IdentityUtilsTestCase(test_utils.TestCase): def test_add_project_owner_option_to_parser(self): parser = argparse.ArgumentParser() cli_identity.add_project_owner_option_to_parser(parser) parsed_args = parser.parse_args(['--project', 'project1', '--project-domain', 'domain1']) self.assertEqual('project1', parsed_args.project) self.assertEqual('domain1', parsed_args.project_domain) def test_find_project(self): sdk_connection = mock.Mock() sdk_find_project = sdk_connection.identity.find_project sdk_find_project.return_value = mock.sentinel.project1 ret = cli_identity.find_project(sdk_connection, 'project1') self.assertEqual(mock.sentinel.project1, ret) sdk_find_project.assert_called_once_with( 'project1', ignore_missing=False, domain_id=None) def test_find_project_with_domain(self): domain1 = mock.Mock() domain1.id = 'id-domain1' sdk_connection = mock.Mock() sdk_find_domain = sdk_connection.identity.find_domain sdk_find_domain.return_value = domain1 sdk_find_project = sdk_connection.identity.find_project sdk_find_project.return_value = mock.sentinel.project1 ret = cli_identity.find_project(sdk_connection, 'project1', 'domain1') self.assertEqual(mock.sentinel.project1, ret) sdk_find_domain.assert_called_once_with( 'domain1', ignore_missing=False) sdk_find_project.assert_called_once_with( 'project1', ignore_missing=False, domain_id='id-domain1') def test_find_project_with_forbidden_exception(self): sdk_connection = mock.Mock() sdk_find_project = sdk_connection.identity.find_project exc = exceptions.HttpException() exc.status_code = 403 sdk_find_project.side_effect = exc ret = cli_identity.find_project(sdk_connection, 'project1') self.assertIsInstance(ret, project.Project) self.assertEqual('project1', ret.id) self.assertEqual('project1', ret.name) def test_find_project_with_generic_exception(self): sdk_connection = mock.Mock() sdk_find_project = sdk_connection.identity.find_project exc = exceptions.HttpException() # Some value other than 403. exc.status_code = 499 sdk_find_project.side_effect = exc with testtools.ExpectedException(exceptions.HttpException): cli_identity.find_project(sdk_connection, 'project1') osc-lib-2.0.0/osc_lib/tests/cli/__init__.py0000664000175000017500000000000013607371235020514 0ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/tests/cli/test_client_config.py0000664000175000017500000002046113607371235022634 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from osc_lib.cli import client_config from osc_lib.tests import utils class TestOSCConfig(utils.TestCase): def setUp(self): super(TestOSCConfig, self).setUp() self.cloud = client_config.OSC_Config() def test_auth_select_default_plugin(self): config = { 'auth_type': 'admin_token', } ret_config = self.cloud._auth_select_default_plugin(config) self.assertEqual('admin_token', ret_config['auth_type']) def test_auth_select_default_plugin_password(self): config = { 'username': 'fred', 'user_id': 'fr3d', } ret_config = self.cloud._auth_select_default_plugin(config) self.assertEqual('password', ret_config['auth_type']) self.assertEqual('fred', ret_config['username']) self.assertEqual('fr3d', ret_config['user_id']) def test_auth_select_default_plugin_password_v2(self): config = { 'identity_api_version': '2', 'username': 'fred', } ret_config = self.cloud._auth_select_default_plugin(config) self.assertEqual('v2password', ret_config['auth_type']) self.assertEqual('fred', ret_config['username']) def test_auth_select_default_plugin_password_v2_int(self): config = { 'identity_api_version': 2, 'username': 'fred', } ret_config = self.cloud._auth_select_default_plugin(config) self.assertEqual('v2password', ret_config['auth_type']) self.assertEqual('fred', ret_config['username']) def test_auth_select_default_plugin_password_v3(self): config = { 'identity_api_version': '3', 'username': 'fred', } ret_config = self.cloud._auth_select_default_plugin(config) self.assertEqual('v3password', ret_config['auth_type']) self.assertEqual('fred', ret_config['username']) def test_auth_select_default_plugin_password_v3_int(self): config = { 'identity_api_version': 3, 'username': 'fred', } ret_config = self.cloud._auth_select_default_plugin(config) self.assertEqual('v3password', ret_config['auth_type']) self.assertEqual('fred', ret_config['username']) def test_auth_select_default_plugin_token(self): config = { 'token': 'subway', } ret_config = self.cloud._auth_select_default_plugin(config) self.assertEqual('token', ret_config['auth_type']) self.assertEqual('subway', ret_config['token']) def test_auth_select_default_plugin_token_v2(self): config = { 'identity_api_version': '2.2', 'token': 'subway', } ret_config = self.cloud._auth_select_default_plugin(config) self.assertEqual('v2token', ret_config['auth_type']) self.assertEqual('subway', ret_config['token']) def test_auth_select_default_plugin_token_v3(self): config = { 'identity_api_version': '3', 'token': 'subway', } ret_config = self.cloud._auth_select_default_plugin(config) self.assertEqual('v3token', ret_config['auth_type']) self.assertEqual('subway', ret_config['token']) def test_auth_v2_arguments(self): config = { 'identity_api_version': '2', 'auth_type': 'v2password', 'auth': { 'username': 'fred', }, } ret_config = self.cloud._auth_v2_arguments(config) self.assertEqual('fred', ret_config['auth']['username']) self.assertFalse('tenant_id' in ret_config['auth']) self.assertFalse('tenant_name' in ret_config['auth']) config = { 'identity_api_version': '3', 'auth_type': 'v3password', 'auth': { 'username': 'fred', 'project_id': 'id', }, } ret_config = self.cloud._auth_v2_arguments(config) self.assertEqual('fred', ret_config['auth']['username']) self.assertFalse('tenant_id' in ret_config['auth']) self.assertFalse('tenant_name' in ret_config['auth']) config = { 'identity_api_version': '2', 'auth_type': 'v2password', 'auth': { 'username': 'fred', 'project_id': 'id', }, } ret_config = self.cloud._auth_v2_arguments(config) self.assertEqual('id', ret_config['auth']['tenant_id']) self.assertFalse('tenant_name' in ret_config['auth']) config = { 'identity_api_version': '2', 'auth_type': 'v2password', 'auth': { 'username': 'fred', 'project_name': 'name', }, } ret_config = self.cloud._auth_v2_arguments(config) self.assertFalse('tenant_id' in ret_config['auth']) self.assertEqual('name', ret_config['auth']['tenant_name']) def test_auth_v2_ignore_v3(self): config = { 'cloud': 'testcloud', 'identity_api_version': '2', 'auth_type': 'v2password', 'auth': { 'username': 'fred', 'project_id': 'id', 'project_domain_id': 'bad', }, } ret_config = self.cloud._auth_v2_ignore_v3(config) self.assertEqual('fred', ret_config['auth']['username']) self.assertFalse('project_domain_id' in ret_config['auth']) def test_auth_default_domain_not_set(self): config = { 'identity_api_version': '3', 'auth_type': 'v3oidcpassword', 'default_domain': 'default', 'auth': { 'username': 'fred', 'project_id': 'id', }, } ret_config = self.cloud._auth_default_domain(config) self.assertEqual('v3oidcpassword', ret_config['auth_type']) self.assertEqual('default', ret_config['default_domain']) self.assertEqual('fred', ret_config['auth']['username']) self.assertNotIn('project_domain_id', ret_config['auth']) self.assertNotIn('user_domain_id', ret_config['auth']) def test_auth_default_domain_use_default(self): config = { 'identity_api_version': '3', 'auth_type': 'v3password', 'default_domain': 'default', 'auth': { 'username': 'fred', 'project_id': 'id', }, } ret_config = self.cloud._auth_default_domain(config) self.assertEqual('v3password', ret_config['auth_type']) self.assertEqual('default', ret_config['default_domain']) self.assertEqual('fred', ret_config['auth']['username']) self.assertEqual('default', ret_config['auth']['project_domain_id']) self.assertEqual('default', ret_config['auth']['user_domain_id']) def test_auth_default_domain_use_given(self): config = { 'identity_api_version': '3', 'auth_type': 'v3password', 'default_domain': 'default', 'auth': { 'username': 'fred', 'project_id': 'id', 'project_domain_id': 'proj', 'user_domain_id': 'use' }, } ret_config = self.cloud._auth_default_domain(config) self.assertEqual('v3password', ret_config['auth_type']) self.assertEqual('default', ret_config['default_domain']) self.assertEqual('fred', ret_config['auth']['username']) self.assertEqual('proj', ret_config['auth']['project_domain_id']) self.assertEqual('use', ret_config['auth']['user_domain_id']) def test_auth_config_hook_default(self): config = {} ret_config = self.cloud.auth_config_hook(config) self.assertEqual('password', ret_config['auth_type']) osc-lib-2.0.0/osc_lib/tests/cli/test_format_columns.py0000664000175000017500000000437313607371235023065 0ustar zuulzuul00000000000000# Copyright 2017 Huawei, Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from osc_lib.cli import format_columns from osc_lib.tests import utils class TestDictColumn(utils.TestCase): def test_dict_column(self): dict_content = { 'key1': 'value1', 'key2': 'value2', } col = format_columns.DictColumn(dict_content) self.assertEqual(dict_content, col.machine_readable()) self.assertEqual("key1='value1', key2='value2'", col.human_readable()) class TestDictListColumn(utils.TestCase): def test_dict_list_column(self): dict_list_content = {'public': ['2001:db8::8', '172.24.4.6'], 'private': ['2000:db7::7', '192.24.4.6']} col = format_columns.DictListColumn(dict_list_content) self.assertEqual(dict_list_content, col.machine_readable()) self.assertEqual('private=192.24.4.6, 2000:db7::7; ' 'public=172.24.4.6, 2001:db8::8', col.human_readable()) class TestListColumn(utils.TestCase): def test_list_column(self): list_content = [ 'key1', 'key2', ] col = format_columns.ListColumn(list_content) self.assertEqual(list_content, col.machine_readable()) self.assertEqual("key1, key2", col.human_readable()) class TestListDictColumn(utils.TestCase): def test_list_dict_column(self): list_dict_content = [ {'key1': 'value1'}, {'key2': 'value2'}, ] col = format_columns.ListDictColumn(list_dict_content) self.assertEqual(list_dict_content, col.machine_readable()) self.assertEqual("key1='value1'\nkey2='value2'", col.human_readable()) osc-lib-2.0.0/osc_lib/tests/__init__.py0000664000175000017500000000000013607371235017745 0ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/tests/api/0000775000175000017500000000000013607371310016411 5ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/tests/api/test_api.py0000664000175000017500000004054313607371235020607 0ustar zuulzuul00000000000000# 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. # """Base API Library Tests""" from keystoneauth1 import exceptions as ksa_exceptions from keystoneauth1 import session from osc_lib.api import api from osc_lib import exceptions from osc_lib.tests.api import fakes as api_fakes class TestBaseAPIDefault(api_fakes.TestSession): def setUp(self): super(TestBaseAPIDefault, self).setUp() self.api = api.BaseAPI() def test_baseapi_request_no_url(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=200, ) self.assertRaises( ksa_exceptions.EndpointNotFound, self.api._request, 'GET', '', ) self.assertIsNotNone(self.api.session) self.assertNotEqual(self.sess, self.api.session) def test_baseapi_request_url(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=200, ) ret = self.api._request('GET', self.BASE_URL + '/qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) self.assertIsNotNone(self.api.session) self.assertNotEqual(self.sess, self.api.session) def test_baseapi_request_url_path(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=200, ) self.assertRaises( ksa_exceptions.EndpointNotFound, self.api._request, 'GET', '/qaz', ) self.assertIsNotNone(self.api.session) self.assertNotEqual(self.sess, self.api.session) def test_baseapi_request_session(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=200, ) ret = self.api._request( 'GET', self.BASE_URL + '/qaz', session=self.sess, ) self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) self.assertIsNotNone(self.api.session) self.assertNotEqual(self.sess, self.api.session) class TestBaseAPIEndpointArg(api_fakes.TestSession): def test_baseapi_endpoint_no_endpoint(self): x_api = api.BaseAPI( session=self.sess, ) self.assertIsNotNone(x_api.session) self.assertEqual(self.sess, x_api.session) self.assertIsNone(x_api.endpoint) self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=200, ) # Normal url self.assertRaises( ksa_exceptions.EndpointNotFound, x_api._request, 'GET', '/qaz', ) # No leading '/' url self.assertRaises( ksa_exceptions.EndpointNotFound, x_api._request, 'GET', 'qaz', ) # Extra leading '/' url self.assertRaises( ksa_exceptions.connection.UnknownConnectionError, x_api._request, 'GET', '//qaz', ) def test_baseapi_endpoint_no_extra(self): x_api = api.BaseAPI( session=self.sess, endpoint=self.BASE_URL, ) self.assertIsNotNone(x_api.session) self.assertEqual(self.sess, x_api.session) self.assertEqual(self.BASE_URL, x_api.endpoint) self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=200, ) # Normal url ret = x_api._request('GET', '/qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) # No leading '/' url ret = x_api._request('GET', 'qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) # Extra leading '/' url ret = x_api._request('GET', '//qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) def test_baseapi_endpoint_extra(self): x_api = api.BaseAPI( session=self.sess, endpoint=self.BASE_URL + '/', ) self.assertIsNotNone(x_api.session) self.assertEqual(self.sess, x_api.session) self.assertEqual(self.BASE_URL, x_api.endpoint) self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=200, ) # Normal url ret = x_api._request('GET', '/qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) # No leading '/' url ret = x_api._request('GET', 'qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) # Extra leading '/' url ret = x_api._request('GET', '//qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) class TestBaseAPIArgs(api_fakes.TestSession): def setUp(self): super(TestBaseAPIArgs, self).setUp() self.api = api.BaseAPI( session=self.sess, endpoint=self.BASE_URL, ) def test_baseapi_request_url_path(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=200, ) ret = self.api._request('GET', '/qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) self.assertIsNotNone(self.api.session) self.assertEqual(self.sess, self.api.session) def test_baseapi_request_session(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=200, ) new_session = session.Session() ret = self.api._request('GET', '/qaz', session=new_session) self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) self.assertIsNotNone(self.api.session) self.assertNotEqual(new_session, self.api.session) class TestBaseAPICreate(api_fakes.TestSession): def setUp(self): super(TestBaseAPICreate, self).setUp() self.api = api.BaseAPI( session=self.sess, endpoint=self.BASE_URL, ) def test_baseapi_create_post(self): self.requests_mock.register_uri( 'POST', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=202, ) ret = self.api.create('qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret) def test_baseapi_create_put(self): self.requests_mock.register_uri( 'PUT', self.BASE_URL + '/qaz', json=api_fakes.RESP_ITEM_1, status_code=202, ) ret = self.api.create('qaz', method='PUT') self.assertEqual(api_fakes.RESP_ITEM_1, ret) def test_baseapi_delete(self): self.requests_mock.register_uri( 'DELETE', self.BASE_URL + '/qaz', status_code=204, ) ret = self.api.delete('qaz') self.assertEqual(204, ret.status_code) class TestBaseAPIFind(api_fakes.TestSession): def setUp(self): super(TestBaseAPIFind, self).setUp() self.api = api.BaseAPI( session=self.sess, endpoint=self.BASE_URL, ) def test_baseapi_find(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz/1', json={'qaz': api_fakes.RESP_ITEM_1}, status_code=200, ) ret = self.api.find('qaz', '1') self.assertEqual(api_fakes.RESP_ITEM_1, ret) self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz/1', status_code=404, ) self.assertRaises( exceptions.NotFound, self.api.find, 'qaz', '1') def test_baseapi_find_attr_by_id(self): # All first requests (by name) will fail in this test self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?name=1', json={'qaz': []}, status_code=200, ) self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?id=1', json={'qaz': [api_fakes.RESP_ITEM_1]}, status_code=200, ) ret = self.api.find_attr('qaz', '1') self.assertEqual(api_fakes.RESP_ITEM_1, ret) # value not found self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?name=0', json={'qaz': []}, status_code=200, ) self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?id=0', json={'qaz': []}, status_code=200, ) self.assertRaises( exceptions.CommandError, self.api.find_attr, 'qaz', '0', ) # Attribute other than 'name' self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?status=UP', json={'qaz': [api_fakes.RESP_ITEM_1]}, status_code=200, ) ret = self.api.find_attr('qaz', 'UP', attr='status') self.assertEqual(api_fakes.RESP_ITEM_1, ret) ret = self.api.find_attr('qaz', value='UP', attr='status') self.assertEqual(api_fakes.RESP_ITEM_1, ret) def test_baseapi_find_attr_by_name(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?name=alpha', json={'qaz': [api_fakes.RESP_ITEM_1]}, status_code=200, ) ret = self.api.find_attr('qaz', 'alpha') self.assertEqual(api_fakes.RESP_ITEM_1, ret) # value not found self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?name=0', json={'qaz': []}, status_code=200, ) self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?id=0', json={'qaz': []}, status_code=200, ) self.assertRaises( exceptions.CommandError, self.api.find_attr, 'qaz', '0', ) # Attribute other than 'name' self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?status=UP', json={'qaz': [api_fakes.RESP_ITEM_1]}, status_code=200, ) ret = self.api.find_attr('qaz', 'UP', attr='status') self.assertEqual(api_fakes.RESP_ITEM_1, ret) ret = self.api.find_attr('qaz', value='UP', attr='status') self.assertEqual(api_fakes.RESP_ITEM_1, ret) def test_baseapi_find_attr_path_resource(self): # Test resource different than path self.requests_mock.register_uri( 'GET', self.BASE_URL + '/wsx?name=1', json={'qaz': []}, status_code=200, ) self.requests_mock.register_uri( 'GET', self.BASE_URL + '/wsx?id=1', json={'qaz': [api_fakes.RESP_ITEM_1]}, status_code=200, ) ret = self.api.find_attr('wsx', '1', resource='qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret) def test_baseapi_find_bulk_none(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.LIST_RESP, status_code=200, ) ret = self.api.find_bulk('qaz') self.assertEqual(api_fakes.LIST_RESP, ret) # Verify headers arg does not interfere ret = self.api.find_bulk('qaz', headers={}) self.assertEqual(api_fakes.LIST_RESP, ret) def test_baseapi_find_bulk_one(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.LIST_RESP, status_code=200, ) ret = self.api.find_bulk('qaz', id='1') self.assertEqual([api_fakes.LIST_RESP[0]], ret) # Verify headers arg does not interfere with search ret = self.api.find_bulk('qaz', id='1', headers={}) self.assertEqual([api_fakes.LIST_RESP[0]], ret) ret = self.api.find_bulk('qaz', id='0') self.assertEqual([], ret) ret = self.api.find_bulk('qaz', name='beta') self.assertEqual([api_fakes.LIST_RESP[1]], ret) ret = self.api.find_bulk('qaz', error='bogus') self.assertEqual([], ret) def test_baseapi_find_bulk_two(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.LIST_RESP, status_code=200, ) ret = self.api.find_bulk('qaz', id='1', name='alpha') self.assertEqual([api_fakes.LIST_RESP[0]], ret) ret = self.api.find_bulk('qaz', id='1', name='beta') self.assertEqual([], ret) ret = self.api.find_bulk('qaz', id='1', error='beta') self.assertEqual([], ret) def test_baseapi_find_bulk_dict(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json={'qaz': api_fakes.LIST_RESP}, status_code=200, ) ret = self.api.find_bulk('qaz', id='1') self.assertEqual([api_fakes.LIST_RESP[0]], ret) class TestBaseAPIList(api_fakes.TestSession): def setUp(self): super(TestBaseAPIList, self).setUp() self.api = api.BaseAPI( session=self.sess, endpoint=self.BASE_URL, ) def test_baseapi_list_no_args(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.LIST_RESP, status_code=204, ) ret = self.api.list('/qaz') self.assertEqual(api_fakes.LIST_RESP, ret) def test_baseapi_list_params(self): params = {'format': 'json'} self.requests_mock.register_uri( 'GET', self.BASE_URL + '?format=json', json=api_fakes.LIST_RESP, status_code=200, ) ret = self.api.list('', **params) self.assertEqual(api_fakes.LIST_RESP, ret) self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?format=json', json=api_fakes.LIST_RESP, status_code=200, ) ret = self.api.list('qaz', **params) self.assertEqual(api_fakes.LIST_RESP, ret) def test_baseapi_list_body(self): self.requests_mock.register_uri( 'POST', self.BASE_URL + '/qaz', json=api_fakes.LIST_RESP, status_code=200, ) ret = self.api.list('qaz', body=api_fakes.LIST_BODY) self.assertEqual(api_fakes.LIST_RESP, ret) def test_baseapi_list_detailed(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz/details', json=api_fakes.LIST_RESP, status_code=200, ) ret = self.api.list('qaz', detailed=True) self.assertEqual(api_fakes.LIST_RESP, ret) def test_baseapi_list_filtered(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?attr=value', json=api_fakes.LIST_RESP, status_code=200, ) ret = self.api.list('qaz', attr='value') self.assertEqual(api_fakes.LIST_RESP, ret) def test_baseapi_list_wrapped(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?attr=value', json={'responses': api_fakes.LIST_RESP}, status_code=200, ) ret = self.api.list('qaz', attr='value') self.assertEqual({'responses': api_fakes.LIST_RESP}, ret) osc-lib-2.0.0/osc_lib/tests/api/__init__.py0000664000175000017500000000000013607371235020516 0ustar zuulzuul00000000000000osc-lib-2.0.0/osc_lib/tests/api/test_utils.py0000664000175000017500000000646713607371235021205 0ustar zuulzuul00000000000000# 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. # """API Utilities Library Tests""" import copy from osc_lib.api import api from osc_lib.api import utils as api_utils from osc_lib.tests.api import fakes as api_fakes class TestBaseAPIFilter(api_fakes.TestSession): """The filters can be tested independently""" def setUp(self): super(TestBaseAPIFilter, self).setUp() self.api = api.BaseAPI( session=self.sess, endpoint=self.BASE_URL, ) self.input_list = [ api_fakes.RESP_ITEM_1, api_fakes.RESP_ITEM_2, api_fakes.RESP_ITEM_3, ] def test_simple_filter_none(self): output = api_utils.simple_filter( ) self.assertIsNone(output) def test_simple_filter_no_attr(self): output = api_utils.simple_filter( copy.deepcopy(self.input_list), ) self.assertEqual(self.input_list, output) def test_simple_filter_attr_only(self): output = api_utils.simple_filter( copy.deepcopy(self.input_list), attr='status', ) self.assertEqual(self.input_list, output) def test_simple_filter_attr_value(self): output = api_utils.simple_filter( copy.deepcopy(self.input_list), attr='status', value='', ) self.assertEqual([], output) output = api_utils.simple_filter( copy.deepcopy(self.input_list), attr='status', value='UP', ) self.assertEqual( [api_fakes.RESP_ITEM_1, api_fakes.RESP_ITEM_3], output, ) output = api_utils.simple_filter( copy.deepcopy(self.input_list), attr='fred', value='UP', ) self.assertEqual([], output) def test_simple_filter_prop_attr_only(self): output = api_utils.simple_filter( copy.deepcopy(self.input_list), attr='b', property_field='props', ) self.assertEqual(self.input_list, output) output = api_utils.simple_filter( copy.deepcopy(self.input_list), attr='status', property_field='props', ) self.assertEqual(self.input_list, output) def test_simple_filter_prop_attr_value(self): output = api_utils.simple_filter( copy.deepcopy(self.input_list), attr='b', value=2, property_field='props', ) self.assertEqual( [api_fakes.RESP_ITEM_1, api_fakes.RESP_ITEM_2], output, ) output = api_utils.simple_filter( copy.deepcopy(self.input_list), attr='b', value=9, property_field='props', ) self.assertEqual([], output) osc-lib-2.0.0/osc_lib/tests/api/fakes.py0000664000175000017500000000252013607371235020061 0ustar zuulzuul00000000000000# 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. # """API Test Fakes""" from keystoneauth1 import session from requests_mock.contrib import fixture from osc_lib.tests import utils RESP_ITEM_1 = { 'id': '1', 'name': 'alpha', 'status': 'UP', 'props': {'a': 1, 'b': 2}, } RESP_ITEM_2 = { 'id': '2', 'name': 'beta', 'status': 'DOWN', 'props': {'a': 2, 'b': 2}, } RESP_ITEM_3 = { 'id': '3', 'name': 'delta', 'status': 'UP', 'props': {'a': 3, 'b': 1}, } LIST_RESP = [RESP_ITEM_1, RESP_ITEM_2] LIST_BODY = { 'p1': 'xxx', 'p2': 'yyy', } class TestSession(utils.TestCase): BASE_URL = 'https://api.example.com:1234/test' def setUp(self): super(TestSession, self).setUp() self.sess = session.Session() self.requests_mock = self.useFixture(fixture.Fixture()) osc-lib-2.0.0/osc_lib/tests/test_shell.py0000664000175000017500000004713413607371235020377 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import copy import mock import os import sys import testtools from osc_lib import shell from osc_lib.tests import utils from openstack.config import loader as config # noqa DEFAULT_AUTH_URL = "http://127.0.0.1:5000/v2.0/" DEFAULT_PROJECT_ID = "xxxx-yyyy-zzzz" DEFAULT_PROJECT_NAME = "project" DEFAULT_DOMAIN_ID = "aaaa-bbbb-cccc" DEFAULT_DOMAIN_NAME = "default" DEFAULT_USER_DOMAIN_ID = "aaaa-bbbb-cccc" DEFAULT_USER_DOMAIN_NAME = "domain" DEFAULT_PROJECT_DOMAIN_ID = "aaaa-bbbb-cccc" DEFAULT_PROJECT_DOMAIN_NAME = "domain" DEFAULT_USERNAME = "username" DEFAULT_PASSWORD = "password" DEFAULT_CLOUD = "altocumulus" DEFAULT_REGION_NAME = "ZZ9_Plural_Z_Alpha" DEFAULT_TOKEN = "token" DEFAULT_SERVICE_URL = "http://127.0.0.1:8771/v3.0/" DEFAULT_AUTH_PLUGIN = "v2password" DEFAULT_INTERFACE = "internal" DEFAULT_COMPUTE_API_VERSION = "" DEFAULT_IDENTITY_API_VERSION = "" DEFAULT_IMAGE_API_VERSION = "" DEFAULT_VOLUME_API_VERSION = "" DEFAULT_NETWORK_API_VERSION = "" LIB_COMPUTE_API_VERSION = "" LIB_IDENTITY_API_VERSION = "" LIB_IMAGE_API_VERSION = "" LIB_VOLUME_API_VERSION = "" LIB_NETWORK_API_VERSION = "" CLOUD_1 = { 'clouds': { 'scc': { 'auth': { 'auth_url': DEFAULT_AUTH_URL, 'project_name': DEFAULT_PROJECT_NAME, 'username': 'zaphod', }, 'region_name': 'occ-cloud,krikkit', 'donut': 'glazed', 'interface': 'public', } } } CLOUD_2 = { 'clouds': { 'megacloud': { 'cloud': 'megadodo', 'auth': { 'project_name': 'heart-o-gold', 'username': 'zaphod', }, 'region_name': 'occ-cloud,krikkit,occ-env', 'log_file': '/tmp/test_log_file', 'log_level': 'debug', 'cert': 'mycert', 'key': 'mickey', } } } PUBLIC_1 = { 'public-clouds': { 'megadodo': { 'auth': { 'auth_url': DEFAULT_AUTH_URL, 'project_name': DEFAULT_PROJECT_NAME, }, 'region_name': 'occ-public', 'donut': 'cake', } } } # The option table values is a tuple of (, , ) # where is the test value to use, is True if this option # should be tested as a CLI option and is True of this option # should be tested as an environment variable. # Global options that should be parsed before shell.initialize_app() is called global_options = { '--os-cloud': (DEFAULT_CLOUD, True, True), '--os-region-name': (DEFAULT_REGION_NAME, True, True), '--os-default-domain': (DEFAULT_DOMAIN_NAME, True, True), '--os-cacert': ('/dev/null', True, True), '--timing': (True, True, False), '--os-interface': (DEFAULT_INTERFACE, True, True) } if shell.osprofiler_profiler: global_options['--os-profile'] = ('SECRET_KEY', True, True) class TestShellArgV(utils.TestShell): """Test the deferred help flag""" def setUp(self): super(TestShellArgV, self).setUp() def test_shell_argv(self): """Test argv decoding Python 2 does nothing with argv while Python 3 decodes it into Unicode before we ever see it. We manually decode when running under Python 2 so verify that we get the right argv types. Use the argv supplied by the test runner so we get actual Python runtime behaviour; we only need to check the type of argv[0] which will alwyas be present. """ with mock.patch( "osc_lib.shell.OpenStackShell.run", self.app, ): # Ensure type gets through unmolested through shell.main() argv = sys.argv shell.main(sys.argv) self.assertEqual(type(argv[0]), type(self.app.call_args[0][0][0])) # When shell.main() gets sys.argv itself it should be decoded shell.main() self.assertEqual(type(u'x'), type(self.app.call_args[0][0][0])) class TestShellHelp(utils.TestShell): """Test the deferred help flag""" def setUp(self): super(TestShellHelp, self).setUp() self.useFixture(utils.EnvFixture()) @testtools.skip("skip until bug 1444983 is resolved") def test_help_options(self): flag = "-h list server" kwargs = { "deferred_help": True, } with mock.patch(self.app_patch + ".initialize_app", self.app): _shell, _cmd = utils.make_shell(), flag utils.fake_execute(_shell, _cmd) self.assertEqual( kwargs["deferred_help"], _shell.options.deferred_help, ) class TestShellOptions(utils.TestShell): """Test the option handling by argparse and openstack.config.loader This covers getting the CLI options through the initial processing and validates the arguments to initialize_app() and occ_get_one() """ def setUp(self): super(TestShellOptions, self).setUp() self.useFixture(utils.EnvFixture()) def test_empty_auth(self): os.environ = {} self._assert_initialize_app_arg("", {}) self._assert_cloud_region_arg("", {}) def test_no_options(self): os.environ = {} self._assert_initialize_app_arg("", {}) self._assert_cloud_region_arg("", {}) def test_global_options(self): self._test_options_init_app(global_options) self._test_options_get_one_cloud(global_options) def test_global_env(self): self._test_env_init_app(global_options) self._test_env_get_one_cloud(global_options) class TestShellCli(utils.TestShell): """Test handling of specific global options _shell.options is the parsed command line from argparse _shell.client_manager.* are the values actually used """ def setUp(self): super(TestShellCli, self).setUp() env = {} self.useFixture(utils.EnvFixture(env.copy())) def test_shell_args_no_options(self): _shell = utils.make_shell() with mock.patch( "osc_lib.shell.OpenStackShell.initialize_app", self.app, ): utils.fake_execute(_shell, "list user") self.app.assert_called_with(["list", "user"]) def test_shell_args_tls_options(self): """Test the TLS verify and CA cert file options""" _shell = utils.make_shell() # Default utils.fake_execute(_shell, "module list") self.assertIsNone(_shell.options.verify) self.assertIsNone(_shell.options.insecure) self.assertIsNone(_shell.options.cacert) self.assertTrue(_shell.client_manager.verify) self.assertIsNone(_shell.client_manager.cacert) # --verify utils.fake_execute(_shell, "--verify module list") self.assertTrue(_shell.options.verify) self.assertIsNone(_shell.options.insecure) self.assertIsNone(_shell.options.cacert) self.assertTrue(_shell.client_manager.verify) self.assertIsNone(_shell.client_manager.cacert) # --insecure utils.fake_execute(_shell, "--insecure module list") self.assertIsNone(_shell.options.verify) self.assertTrue(_shell.options.insecure) self.assertIsNone(_shell.options.cacert) self.assertFalse(_shell.client_manager.verify) self.assertIsNone(_shell.client_manager.cacert) # --os-cacert utils.fake_execute(_shell, "--os-cacert foo module list") self.assertIsNone(_shell.options.verify) self.assertIsNone(_shell.options.insecure) self.assertEqual('foo', _shell.options.cacert) self.assertEqual('foo', _shell.client_manager.verify) self.assertEqual('foo', _shell.client_manager.cacert) # --os-cacert and --verify utils.fake_execute(_shell, "--os-cacert foo --verify module list") self.assertTrue(_shell.options.verify) self.assertIsNone(_shell.options.insecure) self.assertEqual('foo', _shell.options.cacert) self.assertEqual('foo', _shell.client_manager.verify) self.assertEqual('foo', _shell.client_manager.cacert) # --os-cacert and --insecure # NOTE(dtroyer): Per bug https://bugs.launchpad.net/bugs/1447784 # in this combination --insecure now overrides any # --os-cacert setting, where before --insecure # was ignored if --os-cacert was set. utils.fake_execute(_shell, "--os-cacert foo --insecure module list") self.assertIsNone(_shell.options.verify) self.assertTrue(_shell.options.insecure) self.assertEqual('foo', _shell.options.cacert) self.assertFalse(_shell.client_manager.verify) self.assertIsNone(_shell.client_manager.cacert) def test_shell_args_cert_options(self): """Test client cert options""" _shell = utils.make_shell() # Default utils.fake_execute(_shell, "module list") self.assertEqual('', _shell.options.cert) self.assertEqual('', _shell.options.key) self.assertIsNone(_shell.client_manager.cert) # --os-cert utils.fake_execute(_shell, "--os-cert mycert module list") self.assertEqual('mycert', _shell.options.cert) self.assertEqual('', _shell.options.key) self.assertEqual('mycert', _shell.client_manager.cert) # --os-key utils.fake_execute(_shell, "--os-key mickey module list") self.assertEqual('', _shell.options.cert) self.assertEqual('mickey', _shell.options.key) self.assertIsNone(_shell.client_manager.cert) # --os-cert and --os-key utils.fake_execute( _shell, "--os-cert mycert --os-key mickey module list" ) self.assertEqual('mycert', _shell.options.cert) self.assertEqual('mickey', _shell.options.key) self.assertEqual(('mycert', 'mickey'), _shell.client_manager.cert) @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_cloud_no_vendor(self, config_mock): """Test cloud config options without the vendor file""" config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_1)) _shell = utils.make_shell() utils.fake_execute( _shell, "--os-cloud scc module list", ) self.assertEqual( 'scc', _shell.cloud.name, ) # These come from clouds.yaml self.assertEqual( DEFAULT_AUTH_URL, _shell.cloud.config['auth']['auth_url'], ) self.assertEqual( DEFAULT_PROJECT_NAME, _shell.cloud.config['auth']['project_name'], ) self.assertEqual( 'zaphod', _shell.cloud.config['auth']['username'], ) self.assertEqual( 'occ-cloud', _shell.cloud.config['region_name'], ) self.assertEqual( 'occ-cloud', _shell.client_manager.region_name, ) self.assertEqual( 'glazed', _shell.cloud.config['donut'], ) self.assertEqual( 'public', _shell.cloud.config['interface'], ) self.assertIsNone(_shell.cloud.config['cert']) self.assertIsNone(_shell.cloud.config['key']) self.assertIsNone(_shell.client_manager.cert) @mock.patch("openstack.config.loader.OpenStackConfig._load_vendor_file") @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_cloud_public(self, config_mock, public_mock): """Test cloud config options with the vendor file""" config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2)) public_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1)) _shell = utils.make_shell() utils.fake_execute( _shell, "--os-cloud megacloud module list", ) self.assertEqual( 'megacloud', _shell.cloud.name, ) # These come from clouds-public.yaml self.assertEqual( DEFAULT_AUTH_URL, _shell.cloud.config['auth']['auth_url'], ) self.assertEqual( 'cake', _shell.cloud.config['donut'], ) # These come from clouds.yaml self.assertEqual( 'heart-o-gold', _shell.cloud.config['auth']['project_name'], ) self.assertEqual( 'zaphod', _shell.cloud.config['auth']['username'], ) self.assertEqual( 'occ-cloud', _shell.cloud.config['region_name'], ) self.assertEqual( 'occ-cloud', _shell.client_manager.region_name, ) self.assertEqual('mycert', _shell.cloud.config['cert']) self.assertEqual('mickey', _shell.cloud.config['key']) self.assertEqual(('mycert', 'mickey'), _shell.client_manager.cert) @mock.patch("openstack.config.loader.OpenStackConfig._load_vendor_file") @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_precedence(self, config_mock, vendor_mock): config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2)) vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1)) _shell = utils.make_shell() # Test command option overriding config file value utils.fake_execute( _shell, "--os-cloud megacloud --os-region-name krikkit module list", ) self.assertEqual( 'megacloud', _shell.cloud.name, ) # These come from clouds-public.yaml self.assertEqual( DEFAULT_AUTH_URL, _shell.cloud.config['auth']['auth_url'], ) self.assertEqual( 'cake', _shell.cloud.config['donut'], ) # These come from clouds.yaml self.assertEqual( 'heart-o-gold', _shell.cloud.config['auth']['project_name'], ) self.assertEqual( 'zaphod', _shell.cloud.config['auth']['username'], ) self.assertEqual( 'krikkit', _shell.cloud.config['region_name'], ) self.assertEqual( 'krikkit', _shell.client_manager.region_name, ) class TestShellCliPrecedence(utils.TestShell): """Test option precedencr order""" def setUp(self): super(TestShellCliPrecedence, self).setUp() env = { 'OS_CLOUD': 'megacloud', 'OS_REGION_NAME': 'occ-env', } self.useFixture(utils.EnvFixture(env.copy())) @mock.patch("openstack.config.loader.OpenStackConfig._load_vendor_file") @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_precedence_1(self, config_mock, vendor_mock): """Test environment overriding occ""" config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2)) vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1)) _shell = utils.make_shell() # Test env var utils.fake_execute( _shell, "module list", ) self.assertEqual( 'megacloud', _shell.cloud.name, ) # These come from clouds-public.yaml self.assertEqual( DEFAULT_AUTH_URL, _shell.cloud.config['auth']['auth_url'], ) self.assertEqual( 'cake', _shell.cloud.config['donut'], ) # These come from clouds.yaml self.assertEqual( 'heart-o-gold', _shell.cloud.config['auth']['project_name'], ) self.assertEqual( 'zaphod', _shell.cloud.config['auth']['username'], ) # These come from the environment self.assertEqual( 'occ-env', _shell.cloud.config['region_name'], ) self.assertEqual( 'occ-env', _shell.client_manager.region_name, ) @mock.patch("openstack.config.loader.OpenStackConfig._load_vendor_file") @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_precedence_2(self, config_mock, vendor_mock): """Test command line overriding environment and occ""" config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2)) vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1)) _shell = utils.make_shell() # Test command option overriding config file value utils.fake_execute( _shell, "--os-region-name krikkit list user", ) self.assertEqual( 'megacloud', _shell.cloud.name, ) # These come from clouds-public.yaml self.assertEqual( DEFAULT_AUTH_URL, _shell.cloud.config['auth']['auth_url'], ) self.assertEqual( 'cake', _shell.cloud.config['donut'], ) # These come from clouds.yaml self.assertEqual( 'heart-o-gold', _shell.cloud.config['auth']['project_name'], ) self.assertEqual( 'zaphod', _shell.cloud.config['auth']['username'], ) # These come from the command line self.assertEqual( 'krikkit', _shell.cloud.config['region_name'], ) self.assertEqual( 'krikkit', _shell.client_manager.region_name, ) @mock.patch("openstack.config.loader.OpenStackConfig._load_vendor_file") @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_precedence_3(self, config_mock, vendor_mock): """Test command line overriding environment and occ""" config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_1)) vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1)) _shell = utils.make_shell() # Test command option overriding config file value utils.fake_execute( _shell, "--os-cloud scc --os-region-name krikkit list user", ) self.assertEqual( 'scc', _shell.cloud.name, ) # These come from clouds-public.yaml self.assertEqual( DEFAULT_AUTH_URL, _shell.cloud.config['auth']['auth_url'], ) self.assertEqual( 'glazed', _shell.cloud.config['donut'], ) # These come from clouds.yaml self.assertEqual( DEFAULT_PROJECT_NAME, _shell.cloud.config['auth']['project_name'], ) self.assertEqual( 'zaphod', _shell.cloud.config['auth']['username'], ) # These come from the command line self.assertEqual( 'krikkit', _shell.cloud.config['region_name'], ) self.assertEqual( 'krikkit', _shell.client_manager.region_name, ) osc-lib-2.0.0/osc_lib/tests/fakes.py0000664000175000017500000001331113607371235017310 0ustar zuulzuul00000000000000# Copyright 2013 Nebula Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import mock import six import sys from keystoneauth1 import fixture AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" USERNAME = "itchy" USER_ID = "2354b7c1-f681-4c39-8003-4fe9d1eabb65" PASSWORD = "scratchy" PROJECT_NAME = "poochie" PROJECT_ID = "30c3da29-61f5-4b7b-8eb2-3d18287428c7" REGION_NAME = "richie" INTERFACE = "catchy" VERSION = "3" SERVICE_PROVIDER_ID = "bob" TEST_RESPONSE_DICT = fixture.V2Token(token_id=AUTH_TOKEN, user_name=USERNAME) _s = TEST_RESPONSE_DICT.add_service('identity', name='keystone') _s.add_endpoint(AUTH_URL + ':5000/v2.0') _s = TEST_RESPONSE_DICT.add_service('network', name='neutron') _s.add_endpoint(AUTH_URL + ':9696') _s = TEST_RESPONSE_DICT.add_service('compute', name='nova') _s.add_endpoint(AUTH_URL + ':8774/v2') _s = TEST_RESPONSE_DICT.add_service('image', name='glance') _s.add_endpoint(AUTH_URL + ':9292') _s = TEST_RESPONSE_DICT.add_service('object', name='swift') _s.add_endpoint(AUTH_URL + ':8080/v1') TEST_RESPONSE_DICT_V3 = fixture.V3Token(user_name=USERNAME) TEST_RESPONSE_DICT_V3.set_project_scope() TEST_VERSIONS = fixture.DiscoveryList(href=AUTH_URL) def to_unicode_dict(catalog_dict): """Converts dict to unicode dict""" if isinstance(catalog_dict, dict): return {to_unicode_dict(key): to_unicode_dict(value) for key, value in catalog_dict.items()} elif isinstance(catalog_dict, list): return [to_unicode_dict(element) for element in catalog_dict] elif isinstance(catalog_dict, str): return catalog_dict + u"" else: return catalog_dict class FakeStdout(object): def __init__(self): self.content = [] def write(self, text): self.content.append(text) def make_string(self): result = '' for line in self.content: result = result + line return result class FakeLog(object): def __init__(self): self.messages = {} def debug(self, msg): self.messages['debug'] = msg def info(self, msg): self.messages['info'] = msg def warning(self, msg): self.messages['warning'] = msg def error(self, msg): self.messages['error'] = msg def critical(self, msg): self.messages['critical'] = msg class FakeApp(object): def __init__(self, _stdout, _log): self.stdout = _stdout self.client_manager = None self.api_version = {} self.stdin = sys.stdin self.stdout = _stdout or sys.stdout self.stderr = sys.stderr self.log = _log class FakeOptions(object): def __init__(self, **kwargs): self.os_beta_command = False class FakeClientManager(object): def __init__(self): self.compute = None self.identity = None self.image = None self.object_store = None self.volume = None self.network = None self.session = None self.auth_ref = None self.auth_plugin_name = None def get_configuration(self): return { 'auth': { 'username': USERNAME, 'password': PASSWORD, 'token': AUTH_TOKEN, }, 'region': REGION_NAME, 'identity_api_version': VERSION, } class FakeModule(object): def __init__(self, name, version): self.name = name self.__version__ = version # Workaround for openstacksdk case self.version = mock.Mock() self.version.__version__ = version class FakeResource(object): def __init__(self, manager=None, info=None, loaded=False, methods=None): """Set attributes and methods for a resource. :param manager: The resource manager :param Dictionary info: A dictionary with all attributes :param bool loaded: True if the resource is loaded in memory :param Dictionary methods: A dictionary with all methods """ info = info or {} methods = methods or {} self.__name__ = type(self).__name__ self.manager = manager self._info = info self._add_details(info) self._add_methods(methods) self._loaded = loaded def _add_details(self, info): for (k, v) in six.iteritems(info): setattr(self, k, v) def _add_methods(self, methods): """Fake methods with MagicMock objects. For each <@key, @value> pairs in methods, add an callable MagicMock object named @key as an attribute, and set the mock's return_value to @value. When users access the attribute with (), @value will be returned, which looks like a function call. """ for (name, ret) in six.iteritems(methods): method = mock.MagicMock(return_value=ret) setattr(self, name, method) def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) def keys(self): return self._info.keys() osc-lib-2.0.0/osc_lib/exceptions.py0000664000175000017500000000650213607371235017242 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # 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. # """Exception definitions.""" class CommandError(Exception): pass class AuthorizationFailure(Exception): pass class PluginAttributeError(Exception): """A plugin threw an AttributeError while being lazily loaded.""" # This *must not* inherit from AttributeError; # that would defeat the whole purpose. pass class NoTokenLookupException(Exception): """This does not support looking up endpoints from an existing token.""" pass class EndpointNotFound(Exception): """Could not find Service or Region in Service Catalog.""" pass class UnsupportedVersion(Exception): """The user is trying to use an unsupported version of the API""" pass class InvalidValue(Exception): """An argument value is not valid: wrong type, out of range, etc""" message = "Supplied value is not valid" class ClientException(Exception): """The base exception class for all exceptions this library raises.""" def __init__(self, code, message=None, details=None): if not isinstance(code, int) and message is None: message = code code = self.http_status self.code = code self.message = message or self.__class__.message self.details = details def __str__(self): return "%s (HTTP %s)" % (self.message, self.code) class BadRequest(ClientException): """HTTP 400 - Bad request: you sent some malformed data.""" http_status = 400 message = "Bad request" class Unauthorized(ClientException): """HTTP 401 - Unauthorized: bad credentials.""" http_status = 401 message = "Unauthorized" class Forbidden(ClientException): """HTTP 403 - Forbidden: not authorized to access to this resource.""" http_status = 403 message = "Forbidden" class NotFound(ClientException): """HTTP 404 - Not found""" http_status = 404 message = "Not found" class Conflict(ClientException): """HTTP 409 - Conflict""" http_status = 409 message = "Conflict" class OverLimit(ClientException): """HTTP 413 - Over limit: reached the API limits for this time period.""" http_status = 413 message = "Over limit" # NotImplemented is a python keyword. class HTTPNotImplemented(ClientException): """HTTP 501 - Not Implemented: server does not support this operation.""" http_status = 501 message = "Not Implemented" # In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() # so we can do this: # _code_map = dict((c.http_status, c) # for c in ClientException.__subclasses__()) # # Instead, we have to hardcode it: _code_map = dict((c.http_status, c) for c in [ BadRequest, Unauthorized, Forbidden, NotFound, OverLimit, HTTPNotImplemented ]) osc-lib-2.0.0/osc_lib/clientmanager.py0000664000175000017500000002300613607371235017670 0ustar zuulzuul00000000000000# Copyright 2012-2013 OpenStack Foundation # # 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. # """Manage access to the clients, including authenticating when needed.""" import copy import logging import sys from openstack.config import loader as config # noqa from openstack import connection from oslo_utils import strutils import six from osc_lib.api import auth from osc_lib import exceptions LOG = logging.getLogger(__name__) PLUGIN_MODULES = [] class ClientCache(object): """Descriptor class for caching created client handles.""" def __init__(self, factory): self.factory = factory self._handle = None def __get__(self, instance, owner): # Tell the ClientManager to login to keystone if self._handle is None: try: self._handle = self.factory(instance) except AttributeError as err: # Make sure the failure propagates. Otherwise, the plugin just # quietly isn't there. new_err = exceptions.PluginAttributeError(err) six.reraise(new_err.__class__, new_err, sys.exc_info()[2]) return self._handle class ClientManager(object): """Manages access to API clients, including authentication.""" # NOTE(dtroyer): Keep around the auth required state of the _current_ # command since ClientManager has no visibility to the # command itself; assume auth is not required. _auth_required = False def __init__( self, cli_options=None, api_version=None, pw_func=None, app_name=None, app_version=None, ): """Set up a ClientManager :param cli_options: Options collected from the command-line, environment, or wherever :param api_version: Dict of API versions: key is API name, value is the version :param pw_func: Callback function for asking the user for a password. The function takes an optional string for the prompt ('Password: ' on None) and returns a string containing the password :param app_name: The name of the application for passing through to the useragent :param app_version: The version of the application for passing through to the useragent """ self._cli_options = cli_options self._api_version = api_version self._pw_callback = pw_func self._app_name = app_name self._app_version = app_version self.region_name = self._cli_options.region_name self.interface = self._cli_options.interface self.timing = self._cli_options.timing self._auth_ref = None self.session = None # self.verify is the Requests-compatible form # self.cacert is the form used by the legacy client libs # self.insecure is not needed, use 'not self.verify' self.cacert = None self.verify, self.cert = self._cli_options.get_requests_verify_args() # If there is a cacert and we're verifying, it'll be in the verify # argument. if not isinstance(self.verify, bool): self.cacert = self.verify # TODO(mordred) We also don't have any support for setting or passing # in api_timeout, which is set in occ defaults but we skip occ defaults # so set it here by hand and later we should potentially expose this # directly to osc self._cli_options.config['api_timeout'] = None # Get logging from root logger root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) # NOTE(gyee): use this flag to indicate whether auth setup has already # been completed. If so, do not perform auth setup again. The reason # we need this flag is that we want to be able to perform auth setup # outside of auth_ref as auth_ref itself is a property. We can not # retrofit auth_ref to optionally skip scope check. Some operations # do not require a scoped token. In those cases, we call setup_auth # prior to dereferrencing auth_ref. self._auth_setup_completed = False def setup_auth(self): """Set up authentication This is deferred until authentication is actually attempted because it gets in the way of things that do not require auth. """ if self._auth_setup_completed: return # Stash the selected auth type self.auth_plugin_name = self._cli_options.config['auth_type'] # Basic option checking to avoid unhelpful error messages auth.check_valid_authentication_options( self._cli_options, self.auth_plugin_name, ) # Horrible hack alert...must handle prompt for null password if # password auth is requested. if (self.auth_plugin_name.endswith('password') and not self._cli_options.auth.get('password')): self._cli_options.auth['password'] = self._pw_callback() LOG.info('Using auth plugin: %s', self.auth_plugin_name) LOG.debug('Using parameters %s', strutils.mask_password(self._cli_options.auth)) self.auth = self._cli_options.get_auth() if self._cli_options.service_provider: self.auth = auth.get_keystone2keystone_auth( self.auth, self._cli_options.service_provider, self._cli_options.remote_project_id, self._cli_options.remote_project_name, self._cli_options.remote_project_domain_id, self._cli_options.remote_project_domain_name ) self.session = self._cli_options.get_session() self.sdk_connection = connection.Connection(config=self._cli_options) self._auth_setup_completed = True def validate_scope(self): if self._auth_ref.project_id is not None: # We already have a project scope. return if self._auth_ref.domain_id is not None: # We already have a domain scope. return # We do not have a scoped token (and the user's default project scope # was not implied), so the client needs to be explicitly configured # with a scope. auth.check_valid_authorization_options( self._cli_options, self.auth_plugin_name, ) @property def auth_ref(self): """Dereference will trigger an auth if it hasn't already""" if (not self._auth_required or self._cli_options.config['auth_type'] == 'none'): # Forcibly skip auth if we know we do not need it return None if not self._auth_ref: self.setup_auth() LOG.debug("Get auth_ref") self._auth_ref = self.auth.get_auth_ref(self.session) return self._auth_ref def _override_for(self, service_type): key = '%s_endpoint_override' % service_type.replace('-', '_') return self._cli_options.config.get(key) def is_service_available(self, service_type): """Check if a service type is in the current Service Catalog""" # If there is an override, assume the service is available if self._override_for(service_type): return True # Trigger authentication necessary to discover endpoint if self.auth_ref: service_catalog = self.auth_ref.service_catalog else: service_catalog = None # Assume that the network endpoint is enabled. service_available = None if service_catalog: if service_type in service_catalog.get_endpoints(): service_available = True LOG.debug("%s endpoint in service catalog", service_type) else: service_available = False LOG.debug("No %s endpoint in service catalog", service_type) else: LOG.debug("No service catalog") return service_available def get_endpoint_for_service_type(self, service_type, region_name=None, interface='public'): """Return the endpoint URL for the service type.""" # Overrides take priority unconditionally override = self._override_for(service_type) if override: return override if not interface: interface = 'public' # See if we are using password flow auth, i.e. we have a # service catalog to select endpoints from if self.auth_ref: endpoint = self.auth_ref.service_catalog.url_for( service_type=service_type, region_name=region_name, interface=interface, ) else: # Get the passed endpoint directly from the auth plugin endpoint = self.auth.get_endpoint( self.session, interface=interface, ) return endpoint def get_configuration(self): return copy.deepcopy(self._cli_options.config) osc-lib-2.0.0/osc_lib/logs.py0000664000175000017500000001570613607371235016033 0ustar zuulzuul00000000000000# 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. # """Application logging""" import logging import sys import warnings def get_loggers(): loggers = {} for logkey in logging.Logger.manager.loggerDict.keys(): loggers[logkey] = logging.getLevelName(logging.getLogger(logkey).level) return loggers def log_level_from_options(options): # if --debug, --quiet or --verbose is not specified, # the default logging level is warning log_level = logging.WARNING if options.verbose_level == 0: # --quiet log_level = logging.ERROR elif options.verbose_level == 2: # One --verbose log_level = logging.INFO elif options.verbose_level >= 3: # Two or more --verbose log_level = logging.DEBUG return log_level def log_level_from_string(level_string): log_level = { 'critical': logging.CRITICAL, 'error': logging.ERROR, 'warning': logging.WARNING, 'info': logging.INFO, 'debug': logging.DEBUG, }.get(level_string, logging.WARNING) return log_level def log_level_from_config(config): # Check the command line option verbose_level = config.get('verbose_level') if config.get('debug', False): verbose_level = 3 if verbose_level == 0: verbose_level = 'error' elif verbose_level == 1: # If a command line option has not been specified, check the # configuration file verbose_level = config.get('log_level', 'warning') elif verbose_level == 2: verbose_level = 'info' else: verbose_level = 'debug' return log_level_from_string(verbose_level) def set_warning_filter(log_level): if log_level == logging.ERROR: warnings.simplefilter("ignore") elif log_level == logging.WARNING: warnings.simplefilter("ignore") elif log_level == logging.INFO: warnings.simplefilter("once") class _FileFormatter(logging.Formatter): """Customize the logging format for logging handler""" _LOG_MESSAGE_BEGIN = ( '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s ') _LOG_MESSAGE_CONTEXT = '[%(cloud)s %(username)s %(project)s] ' _LOG_MESSAGE_END = '%(message)s' _LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' def __init__(self, options=None, config=None, **kwargs): context = {} if options: context = { 'cloud': getattr(options, 'cloud', ''), 'project': getattr(options, 'os_project_name', ''), 'username': getattr(options, 'username', ''), } elif config: context = { 'cloud': config.config.get('cloud', ''), 'project': config.auth.get('project_name', ''), 'username': config.auth.get('username', ''), } if context: self.fmt = (self._LOG_MESSAGE_BEGIN + (self._LOG_MESSAGE_CONTEXT % context) + self._LOG_MESSAGE_END) else: self.fmt = self._LOG_MESSAGE_BEGIN + self._LOG_MESSAGE_END logging.Formatter.__init__(self, self.fmt, self._LOG_DATE_FORMAT) class LogConfigurator(object): _CONSOLE_MESSAGE_FORMAT = '%(message)s' def __init__(self, options): self.root_logger = logging.getLogger('') self.root_logger.setLevel(logging.DEBUG) # Force verbose_level 3 on --debug self.dump_trace = False if options.debug: options.verbose_level = 3 self.dump_trace = True # Always send higher-level messages to the console via stderr self.console_logger = logging.StreamHandler(sys.stderr) log_level = log_level_from_options(options) self.console_logger.setLevel(log_level) formatter = logging.Formatter(self._CONSOLE_MESSAGE_FORMAT) self.console_logger.setFormatter(formatter) self.root_logger.addHandler(self.console_logger) # Set the warning filter now set_warning_filter(log_level) # Set up logging to a file self.file_logger = None log_file = options.log_file if log_file: self.file_logger = logging.FileHandler(filename=log_file) self.file_logger.setFormatter(_FileFormatter(options=options)) self.file_logger.setLevel(log_level) self.root_logger.addHandler(self.file_logger) # Requests logs some stuff at INFO that we don't want # unless we have DEBUG requests_log = logging.getLogger("requests") # Other modules we don't want DEBUG output for cliff_log = logging.getLogger('cliff') stevedore_log = logging.getLogger('stevedore') iso8601_log = logging.getLogger("iso8601") if options.debug: # --debug forces traceback requests_log.setLevel(logging.DEBUG) else: requests_log.setLevel(logging.ERROR) cliff_log.setLevel(logging.ERROR) stevedore_log.setLevel(logging.ERROR) iso8601_log.setLevel(logging.ERROR) def configure(self, cloud_config): log_level = log_level_from_config(cloud_config.config) set_warning_filter(log_level) self.dump_trace = cloud_config.config.get('debug', self.dump_trace) self.console_logger.setLevel(log_level) log_file = cloud_config.config.get('log_file') if log_file: if not self.file_logger: self.file_logger = logging.FileHandler(filename=log_file) self.file_logger.setFormatter(_FileFormatter(config=cloud_config)) self.file_logger.setLevel(log_level) self.root_logger.addHandler(self.file_logger) logconfig = cloud_config.config.get('logging') if logconfig: highest_level = logging.NOTSET for k in logconfig.keys(): level = log_level_from_string(logconfig[k]) logging.getLogger(k).setLevel(level) if (highest_level < level): highest_level = level self.console_logger.setLevel(highest_level) if self.file_logger: self.file_logger.setLevel(highest_level) # loggers that are not set will use the handler level, so we # need to set the global level for all the loggers for logkey in logging.Logger.manager.loggerDict.keys(): logger = logging.getLogger(logkey) if logger.level == logging.NOTSET: logger.setLevel(log_level) osc-lib-2.0.0/.mailmap0000664000175000017500000000014313607371235014511 0ustar zuulzuul00000000000000 osc-lib-2.0.0/setup.py0000664000175000017500000000200613607371235014602 0ustar zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. # solution from: http://bugs.python.org/issue15881#msg170215 try: import multiprocessing # noqa except ImportError: pass setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) osc-lib-2.0.0/lower-constraints.txt0000664000175000017500000000331513607371235017332 0ustar zuulzuul00000000000000alabaster==0.7.10 amqp==2.1.1 appdirs==1.3.0 Babel==2.3.4 bandit==1.4.0 cachetools==2.0.0 cliff==2.8.0 cmd2==0.8.0 contextlib2==0.4.0 coverage==4.0 debtcollector==1.2.0 decorator==3.4.0 deprecation==1.0 docutils==0.11 dogpile.cache==0.6.2 dulwich==0.15.0 eventlet==0.18.2 extras==1.0.0 fasteners==0.7.0 fixtures==3.0.0 flake8==2.2.4 future==0.16.0 futurist==1.2.0 gitdb==0.6.4 GitPython==1.0.1 greenlet==0.4.10 hacking==0.10.3 imagesize==0.7.1 iso8601==0.1.11 Jinja2==2.10 jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 keystoneauth1==3.7.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.0 mccabe==0.2.1 mock==2.0.0 monotonic==0.6 mox3==0.20.0 msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 openstackdocstheme==1.18.1 openstacksdk==0.15.0 os-service-types==1.2.0 os-testr==1.0.0 oslo.concurrency==3.25.0 oslo.config==5.2.0 oslo.context==2.19.2 oslo.i18n==3.15.3 oslo.log==3.36.0 oslo.messaging==5.29.0 oslo.middleware==3.31.0 oslo.serialization==2.18.0 oslo.service==1.24.0 oslo.utils==3.33.0 oslotest==3.2.0 osprofiler==1.4.0 Paste==2.0.2 PasteDeploy==1.5.0 pbr==2.0.0 pep8==1.5.7 pika==0.10.0 pika-pool==0.1.3 prettytable==0.7.2 pyflakes==0.8.1 Pygments==2.2.0 pyinotify==0.9.6 pyparsing==2.1.0 pyperclip==1.5.27 python-dateutil==2.5.3 python-mimeparse==1.6.0 python-subunit==1.0.0 pytz==2013.6 PyYAML==3.12 reno==2.5.0 repoze.lru==0.7 requests==2.14.2 requests-mock==1.1.0 requestsexceptions==1.2.0 rfc3986==0.3.1 Routes==2.3.1 simplejson==3.5.1 six==1.10.0 smmap==0.9.0 snowballstemmer==1.2.1 Sphinx==1.6.2 sphinxcontrib-websupport==1.0.1 statsd==3.2.1 stestr==1.0.0 stevedore==1.20.0 tenacity==3.2.1 testrepository==0.0.18 testtools==2.2.0 traceback2==1.4.0 unittest2==1.1.0 vine==1.1.4 WebOb==1.7.1 wrapt==1.7.0 osc-lib-2.0.0/README.rst0000664000175000017500000000447413607371235014572 0ustar zuulzuul00000000000000======= osc-lib ======= .. image:: https://img.shields.io/pypi/v/osc-lib.svg :target: https://pypi.org/project/osc-lib/ :alt: Latest Version OpenStackClient (aka OSC) is a command-line client for OpenStack. osc-lib is a package of common support modules for writing OSC plugins. * `PyPi`_ - package installation * `Online Documentation`_ * `Launchpad project`_ - part of OpenStackClient * `Bugs`_ - issue tracking * `Source`_ * `Developer` - getting started as a developer * `Contributing` - contributing code * `Testing` - testing code * IRC: #openstack-sdks on Freenode (irc.freenode.net) * License: Apache 2.0 .. _PyPi: https://pypi.org/project/osc-lib .. _Online Documentation: http://docs.openstack.org/osc-lib/latest/ .. _Launchpad project: https://launchpad.net/python-openstackclient .. _Bugs: https://storyboard.openstack.org/#!/project_group/80 .. _Source: https://opendev.org/openstack/osc-lib .. _Developer: http://docs.openstack.org/project-team-guide/project-setup/python.html .. _Contributing: http://docs.openstack.org/infra/manual/developers.html .. _Testing: http://docs.openstack.org/osc-lib/latest/contributor/#testing .. _Release Notes: https://docs.openstack.org/releasenotes/osc-lib Getting Started =============== osc-lib can be installed from PyPI using pip:: pip install osc-lib Transition From OpenStackclient =============================== This library was extracted from the main OSC repo after the OSC 2.4.0 release. The following are the changes to imports that will cover the majority of transition to using osc-lib: * openstackclient.api.api -> osc_lib.api.api * openstackclient.api.auth -> osc_lib.api.auth * openstackclient.api.utils -> osc_lib.api.utils * openstackclient.common.command -> osc_lib.command.command * openstackclient.common.commandmanager -> osc_lib.command.commandmanager * openstackclient.common.exceptions -> osc_lib.exceptions * openstackclient.common.logs -> osc_lib.logs * openstackclient.common.parseractions -> osc_lib.cli.parseractions * openstackclient.common.session -> osc_lib.session * openstackclient.common.utils -> osc_lib.utils * openstackclient.i18n -> osc_lib.i18n * openstackclient.shell -> osc_lib.shell Also, some of the test fixtures and modules may be used: * openstackclient.tests.fakes -> osc_lib.tests.fakes * openstackclient.tests.utils -> osc_lib.tests.utils osc-lib-2.0.0/requirements.txt0000664000175000017500000000077413607371235016366 0ustar zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 keystoneauth1>=3.7.0 # Apache-2.0 openstacksdk>=0.15.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 simplejson>=3.5.1 # MIT stevedore>=1.20.0 # Apache-2.0 osc-lib-2.0.0/doc/0000775000175000017500000000000013607371310013631 5ustar zuulzuul00000000000000osc-lib-2.0.0/doc/ext/0000775000175000017500000000000013607371310014431 5ustar zuulzuul00000000000000osc-lib-2.0.0/doc/ext/__init__.py0000664000175000017500000000000013607371235016536 0ustar zuulzuul00000000000000osc-lib-2.0.0/doc/ext/apidoc.py0000664000175000017500000000263513607371235016256 0ustar zuulzuul00000000000000# Copyright 2014 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os.path as path from sphinx import apidoc # NOTE(blk-u): pbr will run Sphinx multiple times when it generates # documentation. Once for each builder. To run this extension we use the # 'builder-inited' hook that fires at the beginning of a Sphinx build. # We use ``run_already`` to make sure apidocs are only generated once # even if Sphinx is run multiple times. run_already = False def run_apidoc(app): global run_already if run_already: return run_already = True package_dir = path.abspath(path.join(app.srcdir, '..', '..', 'osc_lib')) source_dir = path.join(app.srcdir, 'api') apidoc.main(['apidoc', package_dir, '-f', '-H', 'osc-lib Modules', '-o', source_dir]) def setup(app): app.connect('builder-inited', run_apidoc) osc-lib-2.0.0/doc/source/0000775000175000017500000000000013607371310015131 5ustar zuulzuul00000000000000osc-lib-2.0.0/doc/source/user/0000775000175000017500000000000013607371310016107 5ustar zuulzuul00000000000000osc-lib-2.0.0/doc/source/user/transition.rst0000664000175000017500000000743113607371235021046 0ustar zuulzuul00000000000000=============================== Transition from OpenStackClient =============================== ``osc-lib`` was extracted from the main OpenStackClient repo after the OSC 2.4.0 release. During the migration all module names have changed from ``openstackclient.*`` to ``osc_lib.*``. In addition, some re-arranging has been done internally to better align modules. The complete list of public module name changes: * ``openstackclient.api.api`` -> ``osc_lib.api.api`` * ``openstackclient.api.auth`` -> ``osc_lib.api.auth`` * ``openstackclient.api.utils`` -> ``osc_lib.api.utils`` * ``openstackclient.common.command`` -> ``osc_lib.command.command`` * ``openstackclient.common.commandmanager`` -> ``osc_lib.command.commandmanager`` * ``openstackclient.common.exceptions`` -> ``osc_lib.exceptions`` * ``openstackclient.common.logs`` -> ``osc_lib.logs`` * ``openstackclient.common.parseractions`` -> ``osc_lib.cli.parseractions`` * ``openstackclient.common.session`` -> ``osc_lib.session`` * ``openstackclient.common.utils`` -> ``osc_lib.utils`` * ``openstackclient.tests.fakes`` -> ``osc_lib.tests.fakes`` * ``openstackclient.tests.utils`` -> ``osc_lib.tests.utils`` Additional Changes ================== In addition to the existing public modules, other parts of OSC have been extracted, including the base ``Command``, ``CommandManager``, ``ClientManager`` and ``Session`` classes. ClientManager ------------- The OSC ``ClientManager`` is responsible for managing all of the handles to the individual API client objects as well as coordinating session and authentication objects. Plugins are encouraged to use the ClientManager interface for obtaining information about global configuration. * ``openstackclient.common.clientmanager`` -> ``osc_lib.clientmanager`` * All of the handling of the ``verify``/``insecure``/``cacert`` configuration options has been consolidated into ``ClientManager``. This converts the ``--verify``, ``--insecure`` and ``--os-cacert`` options into a ``Requests``-compatible ``verify`` attribute and a ``cacert`` attribute for the legacy client libraries. both are now public; the ``_insecure`` attribute has been removed. .. list-table:: Verify/Insecure/CACert :header-rows: 1 * - --verify - --insecure - --cacert - Result * - None - None - - ``verify=True``, ``cacert=None`` * - True - None - None - ``verify=True``, ``cacert=None`` * - None - True - None - ``verify=False``, ``cacert=None`` * - None - None - - ``verify=cacert``, ``cacert=`` * - True - None - - ``verify=cacert``, ``cacert=`` * - None - True - - ``verify=False``, ``cacert=None`` * A number of other ``ClientManager`` attributes have also been made public to encourage their direct use rather than reaching in to the global options passed in the ``ClientManager`` constructor: * ``_verify`` -> ``verify`` * ``_cacert`` -> ``cacert`` * ``_cert`` -> ``cert`` * ``_insecure`` -> removed, use '`not verify'` * ``_interface`` -> ``interface`` * ``_region_name`` -> ``region_name`` Shell ===== * ``openstackclient.shell`` -> ``osc_lib.shell`` * Break up ``OpenStackShell.initialize_app()`` * leave all plugin initialization in OSC in ``_load_plugins()`` * leave all command loading in OSC in ``_load_commands()`` API === The API base layer is the common point for all API subclasses. It is a wrapper around ``keystoneauth1.session.Session`` that fixes the ``request()`` interface and provides simple endpoint handling that is useful when a Service Catalog is either not available or is insufficient. It also adds simple implementations of the common API CRUD operations: create(), delete(), etc. * ``KeystoneSession`` -> merged into ``BaseAPI`` osc-lib-2.0.0/doc/source/user/change_log.rst0000664000175000017500000000004013607371235020727 0ustar zuulzuul00000000000000.. include:: ../../../ChangeLog osc-lib-2.0.0/doc/source/user/index.rst0000664000175000017500000000015213607371235017754 0ustar zuulzuul00000000000000=============== Using osc-lib =============== .. toctree:: :maxdepth: 2 transition change_log osc-lib-2.0.0/doc/source/contributor/0000775000175000017500000000000013607371310017503 5ustar zuulzuul00000000000000osc-lib-2.0.0/doc/source/contributor/index.rst0000664000175000017500000000172413607371235021356 0ustar zuulzuul00000000000000============== Contributing ============== osc-lib utilizes all of the usual OpenStack processes and requirements for contributions. The code is hosted `on OpenStack's Git server`_. `Bug reports`_ and `blueprints`_ may be submitted to the :code:`python-openstackclient` project on `Launchpad`_. Code may be submitted to the :code:`openstack/osc-lib` project using `Gerrit`_. Developers may also be found in the `IRC channel`_ ``#openstack-sdks``. .. _`on OpenStack's Git server`: https://opendev.org/openstack/python-openstackclient/tree .. _Launchpad: https://launchpad.net/python-openstackclient .. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow .. _Bug reports: https://storyboard.openstack.org/#!/project_group/80 .. _blueprints: https://blueprints.launchpad.net/python-openstackclient .. _PyPi: https://pypi.org/project/osc-lib .. _tarball: http://tarballs.openstack.org/osc-lib .. _IRC channel: https://wiki.openstack.org/wiki/IRC osc-lib-2.0.0/doc/source/reference/0000775000175000017500000000000013607371310017067 5ustar zuulzuul00000000000000osc-lib-2.0.0/doc/source/reference/index.rst0000664000175000017500000000016513607371235020740 0ustar zuulzuul00000000000000======================= Library API Reference ======================= .. toctree:: :maxdepth: 2 api/modules osc-lib-2.0.0/doc/source/index.rst0000664000175000017500000000067713607371235017012 0ustar zuulzuul00000000000000========================================= osc-lib -- OpenStackClient Plugin Library ========================================= OpenStackClient (aka OSC) is a command-line client for OpenStack. osc-lib is a package of common support modules for writing OSC plugins. Contents: .. toctree:: :maxdepth: 2 user/index reference/index contributor/index .. rubric:: Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` osc-lib-2.0.0/doc/source/conf.py0000664000175000017500000002046113607371235016441 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # # OpenStack Command Line Client documentation build configuration file, created # by sphinx-quickstart on Wed May 16 12:05:58 2012. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys import openstackdocstheme import pbr.version # NOTE(blk-u): Path for our Sphinx extension, remove when # https://launchpad.net/bugs/1260495 is fixed. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'openstackdocstheme', 'sphinxcontrib.apidoc', ] # openstackdocstheme options repository_name = 'openstack/osc-lib' bug_project = 'python-openstackclient' bug_tag = 'osc-lib' html_last_updated_fmt = '%Y-%m-%d %H:%M' # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'OpenStackClient CLI Base' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # version_info = pbr.version.VersionInfo('osc-lib') # # The short X.Y version. version = version_info.version_string() # The full version, including alpha/beta/rc tags. release = version_info.release_string() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['osc_lib.'] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #html_theme_path = ["."] #html_theme = '_theme' html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] html_theme_path = [openstackdocstheme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'OpenStackCommandLineClientdoc' # -- Options for LaTeX output ------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]) # . latex_documents = [ ('index', 'OpenStackCommandLineClient.tex', u'OpenStack Command Line Client Documentation', u'OpenStack'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). #man_pages = [] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ----------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'OpenStackCommandLineClient', u'OpenStack Command Line Client Documentation', u'OpenStack', 'OpenStackCommandLineClient', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # -- Options for sphinxcontrib.apidoc ---------------------------------------- apidoc_module_dir = '../../osc_lib' apidoc_excluded_paths = [ 'tests/*', ] apidoc_output_dir = 'reference/api' osc-lib-2.0.0/doc/requirements.txt0000664000175000017500000000053113607371235017122 0ustar zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 sphinx!=1.6.6,!=1.6.7,>=1.6.5 # BSD sphinxcontrib-apidoc>=0.2.0 # BSD osc-lib-2.0.0/doc/Makefile0000664000175000017500000001135313607371235015302 0ustar zuulzuul00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html pdf dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " pdf to make pdf with rst2pdf" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." pdf: $(SPHINXBUILD) -b pdf $(ALLSPHINXOPTS) $(BUILDDIR)/pdf @echo @echo "Build finished. The PDFs are in $(BUILDDIR)/pdf." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/NebulaDocs.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/NebulaDocs.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/NebulaDocs" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/NebulaDocs" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." osc-lib-2.0.0/.testr.conf0000664000175000017500000000056713607371235015170 0ustar zuulzuul00000000000000[DEFAULT] test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./osc_lib/tests} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list group_regex=([^\.]+\.)+ osc-lib-2.0.0/LICENSE0000664000175000017500000002363713607371235014112 0ustar zuulzuul00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. osc-lib-2.0.0/HACKING.rst0000664000175000017500000000602713607371235014675 0ustar zuulzuul00000000000000OpenStack Style Commandments ============================ - Step 1: Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/user/hacking.html - Step 2: Read on General ------- - thou shalt not violate causality in our time cone, or else Docstrings ---------- Docstrings should ONLY use triple-double-quotes (``"""``) Single-line docstrings should NEVER have extraneous whitespace between enclosing triple-double-quotes. Deviation! Sentence fragments do not have punctuation. Specifically in the command classes the one line docstring is also the help string for that command and those do not have periods. """A one line docstring looks like this""" Calling Methods --------------- Deviation! When breaking up method calls due to the 79 char line length limit, use the alternate 4 space indent. With the first argument on the succeeding line all arguments will then be vertically aligned. Use the same convention used with other data structure literals and terminate the method call with the last argument line ending with a comma and the closing paren on its own line indented to the starting line level. unnecessarily_long_function_name( 'string one', 'string two', kwarg1=constants.ACTIVE, kwarg2=['a', 'b', 'c'], ) Text encoding ------------- Note: this section clearly has not been implemented in this project yet, it is the intention to do so. All text within python code should be of type 'unicode'. WRONG: >>> s = 'foo' >>> s 'foo' >>> type(s) RIGHT: >>> u = u'foo' >>> u u'foo' >>> type(u) Transitions between internal unicode and external strings should always be immediately and explicitly encoded or decoded. All external text that is not explicitly encoded (database storage, commandline arguments, etc.) should be presumed to be encoded as utf-8. WRONG: infile = open('testfile', 'r') mystring = infile.readline() myreturnstring = do_some_magic_with(mystring) outfile.write(myreturnstring) RIGHT: infile = open('testfile', 'r') mystring = infile.readline() mytext = mystring.decode('utf-8') returntext = do_some_magic_with(mytext) returnstring = returntext.encode('utf-8') outfile.write(returnstring) Python 3.x Compatibility ------------------------ OpenStackClient strives to be Python 3.3 compatible. Common guidelines: * Convert print statements to functions: print statements should be converted to an appropriate log or other output mechanism. * Use six where applicable: x.iteritems is converted to six.iteritems(x) for example. Running Tests ------------- Note: Oh boy, are we behind on writing tests. But they are coming! The testing system is based on a combination of tox and testr. If you just want to run the whole suite, run `tox` and all will be fine. However, if you'd like to dig in a bit more, you might want to learn some things about testr itself. A basic walkthrough for OpenStack can be found at http://wiki.openstack.org/testr osc-lib-2.0.0/releasenotes/0000775000175000017500000000000013607371310015555 5ustar zuulzuul00000000000000osc-lib-2.0.0/releasenotes/source/0000775000175000017500000000000013607371310017055 5ustar zuulzuul00000000000000osc-lib-2.0.0/releasenotes/source/ocata.rst0000664000175000017500000000023013607371235020677 0ustar zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata osc-lib-2.0.0/releasenotes/source/pike.rst0000664000175000017500000000021713607371235020545 0ustar zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike osc-lib-2.0.0/releasenotes/source/stein.rst0000664000175000017500000000022113607371235020732 0ustar zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein osc-lib-2.0.0/releasenotes/source/_static/0000775000175000017500000000000013607371310020503 5ustar zuulzuul00000000000000osc-lib-2.0.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000013607371235022762 0ustar zuulzuul00000000000000osc-lib-2.0.0/releasenotes/source/_templates/0000775000175000017500000000000013607371310021212 5ustar zuulzuul00000000000000osc-lib-2.0.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000013607371235023471 0ustar zuulzuul00000000000000osc-lib-2.0.0/releasenotes/source/unreleased.rst0000664000175000017500000000012613607371235021743 0ustar zuulzuul00000000000000===================== Current Release Notes ===================== .. release-notes:: osc-lib-2.0.0/releasenotes/source/queens.rst0000664000175000017500000000022313607371235021112 0ustar zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens osc-lib-2.0.0/releasenotes/source/train.rst0000664000175000017500000000017613607371235020736 0ustar zuulzuul00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train osc-lib-2.0.0/releasenotes/source/newton.rst0000664000175000017500000000021613607371235021126 0ustar zuulzuul00000000000000============================= Newton Series Release Notes ============================= .. release-notes:: :branch: origin/stable/newton osc-lib-2.0.0/releasenotes/source/index.rst0000664000175000017500000000037113607371235020725 0ustar zuulzuul00000000000000===================== osc-lib Release Notes ===================== .. toctree:: :maxdepth: 1 unreleased train stein rocky queens pike ocata newton Indices and tables ================== * :ref:`genindex` * :ref:`search` osc-lib-2.0.0/releasenotes/source/rocky.rst0000664000175000017500000000022113607371235020737 0ustar zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky osc-lib-2.0.0/releasenotes/source/conf.py0000664000175000017500000002350513607371235020367 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # 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. # osc-lib Release Notes documentation build configuration file, created by # sphinx-quickstart on Thu Jul 28 17:16:41 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import openstackdocstheme # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'reno.sphinxext', 'sphinx.ext.extlinks', ] # Set aliases for extlinks # * lpbug - generic Launchpad bug :lpbug:`123456` # * oscbp - OSC blueprints :oscbp:`Blue Print ` # * oscdoc - OSC Docs :oscdoc:`Comamnd List ` extlinks = { 'lpbug': ( 'https://bugs.launchpad.net/bugs/%s', 'Bug ', ), 'oscbp': ( 'https://blueprints.launchpad.net/python-openstackclient/+spec/%s', '', ), 'oscdoc': ( 'http://docs.openstack.org/developer/osc-lib/%s.html', '', ), } # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'osc-lib Release Notes' copyright = u'2016, osc-lib Developers' # Release notes do not need a version in the title, they span # multiple versions. # The full version, including alpha/beta/rc tags. release = '' # The short X.Y version. version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] html_theme_path = [openstackdocstheme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'OSC_LIBReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [( 'index', 'OSC_LIBReleaseNotes.tex', u'osc-lib Release Notes Documentation', u'osc-lib Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [( 'index', 'osc_libreleasenotes', u'osc-lib Release Notes Documentation', [u'osc-lib Developers'], 1, )] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [( 'index', 'OSC_LIBReleaseNotes', u'osc-lib Release Notes Documentation', u'osc-lib Developers', 'OSC_LIBReleaseNotes', 'Common base library for OpenStackClient plugins.', 'Miscellaneous', )] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] osc-lib-2.0.0/releasenotes/notes/0000775000175000017500000000000013607371310016705 5ustar zuulzuul00000000000000osc-lib-2.0.0/releasenotes/notes/shell-argv-decode-cdc13dc0c4ec07af.yaml0000664000175000017500000000023213607371235025560 0ustar zuulzuul00000000000000--- fixes: - | Decode argv into Unicode on Python 2 in ``OpenStackShell.main()`` [OSC Bug `1603494 `_] osc-lib-2.0.0/releasenotes/notes/os-profile-as-environment-variable-a5e232e9ca7c5171.yaml0000664000175000017500000000025413607371235030616 0ustar zuulzuul00000000000000--- features: - | ``--os-profile`` argument can be loaded from ``OS_PROFILE`` environment variables to avoid repeating ``--os-profile`` in openstack commands.osc-lib-2.0.0/releasenotes/notes/keystone-to-keystone-9b2e55b051775322.yaml0000664000175000017500000000113213607371235025707 0ustar zuulzuul00000000000000--- features: - | Added new options `--os-service-provider`, `--os-remote-project-name`, `--os-remote-project-domain-name`, `--os-remote-project-domain-id`, and `--os-remote-project-id`. These can also be loaded from their respective ENV variables (ex. `OS_SERVICE_PROVIDER` and `OS_REMOTE_PROJECT_ID`) and allow issuing of commands to a federated cloud using keystone-to-keystone identity federation. More information about keystone-to-keystone can be found `here `_. osc-lib-2.0.0/releasenotes/notes/add-KeyValueAppendAction-class-f830e71152d6b91e.yaml0000664000175000017500000000027113607371235027601 0ustar zuulzuul00000000000000--- features: - | Add ``KeyValueAppendAction`` that allows key-value arguments to be specified multiple times. For example: ``--property key1=value1 --property key1=value2``. osc-lib-2.0.0/releasenotes/notes/bug-2004898-686577a07e791051.yaml0000664000175000017500000000041413607371235023137 0ustar zuulzuul00000000000000--- fixes: - Allow using a user ID for authentication in place of a username/domain combination. This fixes the use of ``--os-user-id`` option and ``OS_USER_ID`` environment variable. [Bug `2004898 `_] osc-lib-2.0.0/releasenotes/notes/.placeholder0000664000175000017500000000000013607371235021164 0ustar zuulzuul00000000000000osc-lib-2.0.0/releasenotes/notes/bug-1558690-1528b637f2c0a449.yaml0000664000175000017500000000031213607371235023263 0ustar zuulzuul00000000000000--- fixes: - | Prevent null key setting for key-value pairs in the ``KeyValueAction`` and ``MultiKeyValueAction`` parser actions. [Bug `1558690 `_]osc-lib-2.0.0/releasenotes/notes/arg-precedence-1ba9fd6929650830.yaml0000664000175000017500000000041213607371235024524 0ustar zuulzuul00000000000000--- fixes: - | Add additional precedence fixes to the argument precedence problems in os-client-config 1.18.0 and earlier. This all will be removed when os-client-config 1.19.x is the minimum allwed version in OpenStack's global requirements.txt. osc-lib-2.0.0/releasenotes/notes/bug-1630822-mask-password-on-debug-20dcdf1c54e84fa1.yaml0000664000175000017500000000037013607371235030034 0ustar zuulzuul00000000000000--- security: - | This release contains the fix for `bug 1630822`_ so that passwords are no longer leaked when using the ``--debug`` or ``-vv`` options. .. _bug 1630822: https://bugs.launchpad.net/python-openstackclient/+bug/1630822 osc-lib-2.0.0/releasenotes/notes/drop-py2-support-60c93244107d5778.yaml0000664000175000017500000000020613607371235024706 0ustar zuulzuul00000000000000--- upgrade: - | Python 2.7 support has been dropped. The minimum version of Python now supported by osc-lib is Python 3.6. osc-lib-2.0.0/releasenotes/notes/add-MultiKeyValueCommaAction-class-01dd254a287d70d2.yaml0000664000175000017500000000032713607371235030431 0ustar zuulzuul00000000000000--- feature: - | Add ``MultiKeyValueCommaAction`` as a ``MultiKeyValueAction`` sublass that allows values to include a comma. For example: ``--property key1=value1,value2,key2=value3,value4,value5``. osc-lib-2.0.0/releasenotes/notes/1.0-summary-47dcce446d6a512b.yaml0000664000175000017500000000522313607371235024064 0ustar zuulzuul00000000000000--- prelude: > ``osc-lib`` was extracted from the main OpenStackClient repo after the OSC 2.4.0 release. A number of the lower-layer modules were simply renamed into the osc_lib namespace:: * openstackclient.api.api -> osc_lib.api.api * openstackclient.api.auth -> osc_lib.api.auth * openstackclient.api.utils -> osc_lib.api.utils * openstackclient.common.command -> osc_lib.command.command * openstackclient.common.exceptions -> osc_lib.exceptions * openstackclient.common.logs -> osc_lib.logs * openstackclient.common.parseractions -> osc_lib.cli.parseractions * openstackclient.common.session -> osc_lib.session * openstackclient.common.utils -> osc_lib.utils * openstackclient.i18n -> osc_lib.i18n The higher-layer components, such as the OpenStackShell and ClientManager objects, have had significant changes made to them to streamline interaction with ``os-client-config`` and ``keystoneauth`` in addition to the rename:: * openstackclient.common.commandmanager -> osc_lib.command.commandmanager * openstackclient.shell -> osc_lib.shell features: - Add ``utils.find_min_match()`` function to filter a list based on a set of minimum values of attributes. For example, selecting all compute flavors that have a minimum amount of RAM and disk and VCPUs. - Add ``cli.client_config.OSC_Config`` as a subclass of ``os_client_config.config.OpenStackConfig`` to collect all of the configuration option special cases in OSC into one place and insert into the ``os-client-config`` handling. fixes: - The ``parseractions.KeyValueAction`` class now raises a ``argparse.ArgumentTypeError`` exception when the argument is not in the form ``=``. - Change ``utils.find_resource()`` to handle client managers that lack a ``find()`` method. Raise an ``exceptions.CommandError`` exception when multiple matches are found. - Change ``utils.find_resource()`` to handle glanceclient's ``HTTPNotFound`` exception. - Change ``utils.find_resource()`` to attempt lookups as IDs first, falling back to ``find()`` methods when available. - Refactor ``ClientManager`` class to remove OSC-specific logic and move all option special-cases into ``cli.client_config.OSC_Config``. Also change some private attributes to public (``region_name``, ``interface``, ``cacert``, ``verify`` and remove ``_insecure``). - Refactor ``OpenStackShell`` to handle only global argument processing and setting up the ClientManager with configuration from ``os-client-config``. Command and plugin loading remain in OSC. osc-lib-2.0.0/releasenotes/notes/auth-type-none-d96760912605f822.yaml0000664000175000017500000000032013607371235024375 0ustar zuulzuul00000000000000--- fixes: - | Fixes incorrect error when using ``auth_type: none`` in ``clouds.yaml``. - | Respects the ``_endpoint_override`` configuration options, similarly to openstacksdk. osc-lib-2.0.0/releasenotes/notes/bug-2006480-436489d39643e76c.yaml0000664000175000017500000000023513607371235023216 0ustar zuulzuul00000000000000--- fixes: - Fix format_columns.format_dict() to properly re-format nested dicts. [Bug `2006480 `_] osc-lib-2.0.0/releasenotes/notes/find-project-203bf867619c557e.yaml0000664000175000017500000000070213607371235024252 0ustar zuulzuul00000000000000--- features: - | Adds ``osc_lib.cli.identity.find_project()``. This function can be used to look up a project ID from command-line options like: .. code-block:: python find_project(self.app.client_manager.sdk_connection, parsed_args.project, parsed_args.project_domain) - | Adds ``osc_lib.cli.identity.add_project_owner_option_to_parser()`` to register project and project domain options to CLI. osc-lib-2.0.0/releasenotes/notes/direct-openstacksdk-535a179f3c645cc0.yaml0000664000175000017500000000017713607371235025702 0ustar zuulzuul00000000000000--- upgrade: - | The dependency on ``os-client-config`` has been removed in favor of direct use of ``openstacksdk``. osc-lib-2.0.0/releasenotes/notes/none-auth-cli-48ab0e48d4852941.yaml0000664000175000017500000000055313607371235024327 0ustar zuulzuul00000000000000--- fixes: - | It is now possible to specify the ``none`` auth type (via ``--os-auth-type`` CLI argument or ``OS_AUTH_TYPE`` environment variable). To use it, ``--os-endpoint`` CLI argument or ``OS_ENDPOINT`` environment variable must be specified. See `the bug `_ for more detail.osc-lib-2.0.0/.zuul.yaml0000664000175000017500000000154113607371235015034 0ustar zuulzuul00000000000000- project: templates: - check-requirements - lib-forward-testing - lib-forward-testing-python3 - openstack-lower-constraints-jobs - openstack-python3-ussuri-jobs - osc-tox-unit-tips - publish-openstack-docs-pti - release-notes-jobs-python3 check: jobs: - osc-functional-devstack: required-projects: - openstack/python-openstackclient - osc-functional-devstack-tips: voting: false # The functional-tips job only tests the latest and shouldn't be run # on the stable branches branches: ^(?!stable) required-projects: - openstack/python-openstackclient gate: jobs: - osc-functional-devstack: required-projects: - openstack/python-openstackclient osc-lib-2.0.0/test-requirements.txt0000664000175000017500000000120013607371235017324 0ustar zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD oslotest>=3.2.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 sphinx!=1.6.6,>=1.6.2 # BSD os-testr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testtools>=2.2.0 # MIT osprofiler>=1.4.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 # Documentation openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 osc-lib-2.0.0/ChangeLog0000664000175000017500000010172113607371310014640 0ustar zuulzuul00000000000000CHANGES ======= 2.0.0 ----- * Add a release note for dropping py2 * Drop python 2.7 support and testing * Add enhance\_help kwarg to tags option generators * Switch to Ussuri jobs * Add KeyValueAppendAction to osc-lib 1.15.0 ------ * Update master for stable/train 1.14.1 ------ * format\_dict() returns no value for None * Handle nested dicts in format\_dict() 1.14.0 ------ * Cleanups to prepare for OSC 4 release * Fix usage of NotFound exception * Add error message when occurrence Forbidden error * Add Python 3 Train unit tests 1.13.0 ------ * Corrent using auth\_type=none in clouds.yaml * Add FakeModule from OSC * Move the \_tag module from OSC to osc-lib * Final switch to stestr * Dropping the py35 testing * Updates for OpenDev transition * OpenDev Migration Patch * Update master for stable/stein 1.12.1 ------ * Add release note for --os-user-id fix * add python 3.7 unit test job * Allow use of user-id as well as username for authentication 1.12.0 ------ * Use template for lower-constraints * Change openstack-dev to openstack-discuss * Default to using public interface for all clients * Fix formatter handling for python 3.7 * Remove pypy job * Mask password when specified as an argument with a space * add lib-forward-testing-python3 test job * switch documentation job to new PTI * add python 3.6 unit test job * import zuul job settings from project-config * Revert "Fixes a bug with issubclass() being called on an instance" * Remove PyPI downloads * Replaced a few references to LP bugs with storyboard * Update reno for stable/rocky 1.11.0 ------ * Fixes a bug with issubclass() being called on an instance * Stop processing requests verify args * Replace autodoc with sphinxcontrib-apidoc * Run OSC unit test tips jobs * Pass prompt\_for\_password to OSC\_Config constructor * Fix exception in log warning when no cloud config exists * Use openstack.config for config and Session objects * Update sphinx lower-constraint to match test-requirements * Adding api\_version to FakeAPP * Add release note link in README * Add cloud to warning about domain related config * Delete the output in "find\_bulk" api * fix tox python3 overrides * Add OpenTracing based driver id * Trivial: Update pypi url to new url * Rename python-openstacksdk to openstacksdk * add lower-constraints job 1.10.0 ------ * Allow to use the none auth plugin * Set defaults in ClientManager tests * Updated from global requirements * Zuul: Remove project name * Zuul: Remove project name * Update reno for stable/queens * Updated from global requirements * Updated from global requirements * Suppress subTest() expected errors 1.9.0 ----- * Fix find() interface when attr is not specified * Relocate utils tests to match the main code * Add project lookup utils * Updated from global requirements * Fix sorting in Python 3 1.8.0 ----- * Add utils for better column handling * Updated from global requirements * Use converged SDK insead of os-client-config * Remove -U from pip install * Avoid tox\_install.sh for constraints support * Util to calculate header and attr names based on parsed\_args.columns * Make -tips job non-voting * Remove setting of version/release from releasenotes * Updated from global requirements * Updated from global requirements * Consume the devstack functional jobs from OSC * Migrade legacy jobs into the repo * Add CLI/ENV options and documentation for keystone-to-keystone * Updated from global requirements * --os-profile option suddenly causes trouble in unit tests * Updates for stestr * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Update reno for stable/pike * Updated from global requirements * Update the documentation link for doc migration * Updated from global requirements * Updated from global requirements * Emit warnings to encourage cliff FormattableColumn 1.7.0 ----- * Add test methods to compare formattable column values * Updated from global requirements * use openstackdocstheme html context * update links to docs in readme * switch from oslosphinx to openstackdocstheme * turn on warning-is-error for sphinx * rearrange existing documentation to fit the new standard layout * Optimize find\_resource: do not run the same query twice * Make --os-profile load from environment variables * Deprecate --profile and remove after Apr 2017 * Updated from global requirements 1.6.0 ----- * Fix shell saving OpenStackConfig object * Follow-up to headers handler to fix find\_bulk() * Updated from global requirements * Updated from global requirements 1.5.1 ----- * Tell ClientManager when auth is required * Add endpoint hook to BaseAPI 1.5.0 ----- * Add token auth test * Revert "Avoid to authenticate twice" 1.4.0 ----- * Add MultiKeyValueCommaAction to osc-lib * Add more API support * Add formattable column base classes * Updated from global requirements * Updated from global requirements * Avoid to authenticate twice * Change noauth strategy for plugin loading * Nit: Reorder some util methods in alphabetic order * Using assertIsNone() instead of assertEqual(None) * Remove log translations * Fix find\_resource exception handling on numeric names with kwargs * Util methods for column name backward compatibility * The python 3.5 is added * Updated from global requirements * Updated from global requirements * Pass ssl values through to OCC * Avoid 'NoneType' error when format conversion * Updated from global requirements * Update reno for stable/ocata * Updated from global requirements * Updated from global requirements 1.3.0 ----- * To display image size in human friendly format * Don't call formatters on None value * Include OSC additions 2 * Include OSC additions * Add deprecated\_option\_warning() method to Command * Calls to federated service providers using Keystone-to-Keystone * Add Constraints support * Updated from global requirements * Updated from global requirements * Updated from global requirements * Don't include openstack/common in flake8 exclude list * Fix version import in releasenotes * Remove os-client-config hacks for 1.19.x and 1.20.x * Updated from global requirements * Updated from global requirements * Allow passing app\_name and app\_version for useragent * Avoid string actions on non-string objects * Updated from global requirements * Updated from global requirements * Updated from global requirements 1.2.0 ----- * Add release note for security bug 1630822 * Improve output of supported client versions * Enable release notes translation * Updated from global requirements * Mask passwords in debug logs for auth\_config\_hook * Updated from global requirements * Fix a tiny typo in documentation * Updated from global requirements * Updated from global requirements * TrivilalFix: Using assertIsNone() instead of assertEqual(None) * Updated from global requirements * Update docstring to match params * Incorrect usage message when no auth param passed * standardize release note page ordering * Update reno for stable/newton * Updated from global requirements * Prompt for auth options 1.1.0 ----- * Fix default handling for verify option in ClientManager * Clean imports in code * TrivialFix: Remove logging import unused * Updated from global requirements * Updated from global requirements * Updated from global requirements 1.0.2 ----- * Another precedence fix 1.0.1 ----- * More hacks to fix broken o-c-c precedence 1.0.0 ----- * Fix arguemnt precedence issues with os-client-config * Do not add user domain options if not needed * Updated from global requirements * Decode argv to Unicode on py2 * Prevent null key setting for property * Add release notes for 1.0 release * Updated from global requirements * Updated from global requirements * Add reno for osc-lib release notes management * Updated from global requirements 0.4.1 ----- * Allow shell class to be overridden in test subclass * Remove option handling unused code * Use assertEqual() instead of assertDictEqual() 0.4.0 ----- * Add Python 3.5 classifier and venv * Updated from global requirements * Fix v2 auth with v3 args present * trivial whitespace change to kick off docs publishing * Remove discover from test-requirements * Remove unused releasenotes infrastructure * Remove discover from test-reqs 0.3.0 ----- * Updated from global requirements * Generate auth plugin options based on the name * Remove tempest from test-requirements.txt * Don't create a requests.Session for session * Remove old fakes * Remove setting project name on clientmanager * Updated from global requirements 0.2.1 ----- * Get VersionInfo of "osc-lib" * Attempt to find resource by ID, without kwargs * Make OSC\_Config the default 0.2.0 ----- * Updated from global requirements * update the transition docs * Add OSC\_Config os-client-config subclass * Updated from global requirements * Backport scope defaults fix (bug 1582774) * Backport check\_valid\_auth\_options() fix * Change default auth plugin to 'password' * Backport scoped token fixes (bug 1592062) * Backport i18n fixes (bug 1574965) * Backport TypeError fix (bug 1575787) * Move api/auth.py into osc\_lib * Updated from global requirements * Modify find\_resource to support glanceclient HTTPNotFound exception * Update untils\_find\_resource to support no unique matches error * Updated from global requirements * Update is\_ascii to work with py3 * Clean up foundation copyrights * Add find\_min\_match() * Sort utils.py and test\_utils.py * Fix interface arg to url\_for() * Fix missing i18n supports in osc-lib 0.1.0 ----- * Backport i18n fixes * Backport log fix * Backport --os-beta-command * Error handling for KeyValueAction class * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Change is\_network\_endpoint\_enabled() to is\_service\_available() * Clean up API * Move api.api and api.utils to osc\_lib * Move shell to osc\_lib and begin rework * Add transition doc * Rework TLS option handling * Remove keystoneclient dependency * Move clientmanager to osc\_lib * Updated from global requirements * Updated from global requirements * fix the docs build * Fix imports in remaining openstackclient modules for testing * Begin moving bits to osc\_lib * Make remaining tests pass * Trim requirements.txt and test-requirements.txt * Rename to osc-lib * Implement "address scope set" command * Implement "address scope show" command * Implement "address scope list" command * Implement "address scope delete" command * Implement "address scope create" command * Updated from global requirements * Ignore domain related config when using with keystone v2 * Updated from global requirements * Ignore domain related config when using with keystone v2 * remove assert in favor an if/else * Replace tempest-lib with tempest.lib * add a bandit environment to tox * Support for volume service list * Updated from global requirements * Add "server group show" command * Add "server group list" command * Add "server group delete" command * Add "server group create" command * Fix mutable default arguments in tests * Rename --profile to --os-profile * Updated from global requirements * Updated from global requirements * Propagate AttributeErrors when lazily loading plugins * Updated from global requirements * Move keys() methods in each resource class to FakeResource * Updated from global requirements * Updated from global requirements * Support client certificate/key * Fix typos in docstrings and comments * Use fixtures and addCleanup instead of tearDown * Don't mask authorization errors * Remove unused method 'from\_response' * Refactor security group rule list to use SDK * Add "aggregate unset" to osc * Subnet: Add "subnet set" command using SDK * [Floating IP] Neutron support for "ip floating create" command * Refactor security group rule create to use SDK * Add Subnet add/remove support to router * Add "router remove port" to osc * Add "router add port" to osc * Updated from global requirements * update docs with status of plugins * Updated from global requirements * Use assertItemsEqual() instead of assertListEqual() * Fix dict.keys() compatibility for python 3 * Add "os subnet create" command using SDK * Refactor security group create to use SDK * Refactor security group show to use SDK * Add 'port set' command * [Subnet pool] Add 'subnet pool create' command support * [Subnet pool] Add 'subnet pool set' command support * remove py26 workaround in osc * Add port list command * Trivial: Remove useless return * Updated from global requirements * Add 'port create' command * Updated from global requirements * Updated from global requirements * Refactor security group set to use SDK * Updated from global requirements * Fix regression in interactive client mode * Subnet: Add "subnet delete" command using SDK * fix: Exception message includes unnecessary class args * Refactor security group list to use SDK * Add MultiKeyValueAction to custom parser action * Updated from global requirements * [compute] Add set host command * Add shell --profile option to trigger osprofiler from CLI * Floating IP: Neutron support for "ip floating show" command * Improve tox to show coverage report on same window * Updated from global requirements * Defaults are ignored with flake8 * Fixed a bunch of spacing * Add "security group rule show" command * [compute] Support restore server * Use instanceof instead of type * Add "os subnet show" command using SDK * Clean redundant argument to dict.get * Updated from global requirements * Fix Mutable default argument * gitignore .idea * Replace string format arguments with function parameters * Support unscoped token request * Don't use Mock.called\_once\_with that does not exist * Subnet Pool: Add "subnet pool show" command * Subnet Pool: Add "subnet pool list" command * Remove unused test-requirments * Subnet Pool: Add "subnet pool delete" command * Fix 'openstack --help' fails if clouds.yaml cannot be read * Floating IP: Neutron support for "ip floating list" command * Floating IP: Neutron support for "ip floating delete" command * Updated from global requirements * Updated from global requirements * Refactor security group rule delete to use SDK * Add "token revoke" for identity v3 * Fix DisplayCommandBase comments for cliff Lister subclass tests * Updated from global requirements * Add support for triggering an crash dump * Allow custom log levels for other loggers * Use assert\_not\_called() in common tests * Fix a spell typos * Refactor security group delete to use SDK * Add "os port show" command * Drop log\_method decorator * Updated from global requirements * log take\_action parameters in a single place * Update translation setup * Allow wait\_for\_delete to work for all clients * Updated from global requirements * Remove the Tuskar client * Updated from global requirements * Subnet List * Updated from global requirements * Updated from global requirements * log\_method: get logger from decorated method if unspecified * Set up logger of each command by metaclass * Add support to delete the ports * Updated from global requirements * Refactor "os availability zone list" * Changed the abstract columns and datalists from test cases of common and Identity * Updated from global requirements * Use assertTrue/False instead of assertEqual(T/F) * Updated from global requirements * Delete the unused LOG configure code * Refactor network endpoint enablement checking * Implementation for project unset cmd for python-openstackclient * Trivial: Remove useless string\_to\_bool() * Refactor: Initialize parser in setUp() in TestNonNegativeAction * Refactor: Initialize parser in setUp() in TestKeyValueAction * Replace assertEqual(None, \*) with assertIsNone(\*) * Improve output for "os security group show" * Add all regions to cloud configuration * Updated from global requirements * Router: Add "router show" command using SDK * Router: Add "router set" command using SDK * Updated from global requirements * Router: Add "router delete" command using SDK * Router: Add "router create" command using SDK * Updated from global requirements * Deprecated tox -downloadcache option removed * Router: Add "router list" command using SDK * Remove python-neutronclient requirement * The format\_exc method does not take an exception * Updated from global requirements * SDK integration: Add a temporary method to create network client using sdk * Updated from global requirements * Add reno for release notes management * Switch to ksa Session * autodocument commands from plugins using stevedore.sphinxext * Updated from global requirements * Change the home-page value in setup.cfg * Add "openstack server unshelve" into OSC * Trivial: Fix a typo * Move FakeServer to tests.common.v2.compute.fakes * Trivial: Add missing doc for parameter in wait\_for\_delete() * Remove py26 support * Add "openstack server shelve" into OSC * Trivial: Fix wrong doc for wait\_for\_status() * Updated from global requirements * Trivial: Remove doc for non-existing param in format\_dict() * Introduce random server faking mechanism * Enable FakeResource to fake methods * Allow error status to be specified * Remove deprecated 'project usage list' command * Remove LICENSE APPENDIX * validate non-ascii values for swift properties * Fix the bug of "openstack console log show" * Add "server stop" command to osc * Add "server start" command to osc * Allow int version numbers in the clouds.yaml * Rename context.py to logs.py * Allow debug to be set in configuration file * Updated from global requirements * Fix issue when displaying image\_member * Updated from global requirements * Add compute service delete * Updated from global requirements * Move session and fixtures to keystoneauth1 * Remove cliff-tablib from requirements.txt * Updated from global requirements * Updated from global requirements * Mask the sensitive values in debug log * Updated from global requirements * add set/unset support for objects in object store * add support for set/unset of container properties * Updated from global requirements * Rename swift account commands * Add support for showing account details * Add support for updating swift account properties * Add tests for find\_resource() * attempt to find resource by listing * Additional exception handling for find\_resource * Add image create support for image v2 * Change ignore-errors to ignore\_errors * Move option logging back to start if initialize\_app() * Set default auth plugin back to 'password' * Updated from global requirements * Updated from global requirements * Fix compute API version snafu * Use a common decorator to log 'take\_action' activation * Fix 'auhentication' spelling error/mistake * Create log configuration class * Override the debug default and help text * Move options to log level out of shell.py * Move set warnings filters to logging module * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Remove non-existing hacking deviations from doc * Set up every time record log in file * Alphabetize setup.cfg * Add set feature to volume type v2 * Add list feature to volume v2 * Updated from global requirements * Minor Documentation changes for code samples * Updated from global requirements * Add support for volume v2 commands * Add configuration show command * Add plugin interface version * Updated from global requirements * Add create and list for volume type v2 * add image member commands for image API * Fix the way auth\_type default value is overriden * Rename type.py to volume\_type.py * Add volume type show for volume v1 * More minor docs fixes * Do not set default versions in parsed args * Updated from global requirements * Remove requirements.txt from tox.ini * Rename endpoint type to interface * Updated from global requirements * temporarily skip help tests * Drop py33 support for Liberty * Fix interactive password prompt * Updated from global requirements * Fix wrong mock method call * add functional tests for identity v3 * Add --os-endpoint-type cli optional argument * Updated from global requirements * Add support for volume API v2 QoS commands * Alphabetize QoS specs * Add support for volume API v1 QoS commands * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Improve the hint message * Fix the typo in \`openstackclient/shell.py\` * Skip trying to set project\_domain\_id if not using password * Updated from global requirements * Updated from global requirements * Enables retrieval of project's parents and subtree * Add support for volume backup v2 command * Updated from global requirements * Add support for volume snapshot v2 command * Allow --insecure to override --os-cacert * Add EC2 support for identity v3 API * Get rid of oslo\_i18n deprecation notice * Rework shell tests * Ignore cover directory from git * Set tenant options on parsed namespace * Add support for volume v2 API * Add --wait to server delete * Use ostestr for test runs * Add cli tests for --verify and friends * Fix shell tests * Add support for v2 image set command * Remove oslo serialization requirement * Fix insecure/verify options * Use format options for functional tests * Fix functional test gate * Updated from global requirements * Send the correct user-agent to Keystone * Updated from global requirements * Updated from global requirements * Updated from global requirements * Add os-client-config cli tests * minor syntax error in tox.ini * Minor logging/debug cleanups * Raise exception if no session is created * Functional tests run in many environments * Remove references to venv * Add --os-cloud support * Print warning on authentication error * Uncap library requirements for liberty * Defer client imports * Federation Service Providers CRUD operations * Add warning message if unknown version supplied * Fix session timing * Suppress warnings user can't fix * Updated from global requirements * Add the ability to set and unset flavor properties * Use cliff deferred help instead of homemade one * Move OSC auth plugins so they can be found * Add identity v3 catalog show * Add identity v3 catalog list * Add ability for diplaying hypervisor statistics * Raise AttributeError for unknown attributes * Updated from global requirements * Fix auth-required for help command * change oslo namespace to avoid warning * Updated from global requirements * Fix error msg in sort\_items * Implement trust in identity v3 api * Check volume status before extending size * Adding default user\_domain\_id parameter only when using password auth * Add sort support to image list * Updated from global requirements * Change test order to avoid incompatibliity * Add filter to image list * fix the wrong order of assertEqual args * Remove ignore portion of tox.ini * Begin low-level API for Image v1 and v2 * Add missing oslo-config to requirements * Default user domain id and project domain id * Add helpful messages when authN'ing with password * Add version url config workaround * Deprecate project usage list command * Updated from global requirements * Upgrade hacking to 0.10 * Updated from global requirements * Fix up snapshot command * Rename \`os project usage list\` to \`os usage list\` * Add usage show command * Don't import form keystoneclient.openstack.common * list availability zones for compute * Updated from global requirements * Updated from global requirements * Enhance the theming for modules page * add keystone v3 region object * Updated from global requirements * Add environment variable in the os-auth-type help * Liberalize version matching a bit * Tests work fine with random PYTHONHASHSEED * Updated from global requirements * Add --or-show option to user create * Add cliff-tablib to requirements * Use fixtures from keystoneclient for static data * Unscoped federated user-specific commands * Adjust some logging levels * Change --os-auth-plugin to --os-auth-type * Include support for using oslo debugger in tests * Clean up shell authentication * only generate one clientmanager instance in interactive mode * Remove ClientManager.\_service\_catalog * Remove now-unnecessary client creation hacks * use jsonutils in oslo.serialization instead of keystoneclient * Close files on server create, add tests * Move plugin stuff to clientmanager * Put pbr and six first in requirements list * Add plugin to support token-endpoint auth * Updated from global requirements * Fix operation on clouds with availability-zones * Add translation markers for user v2 actions * Add domain parameters to user show for Identity V3 * Support for keystone auth plugins * Add 'command list' command * CRUD operations for federated protocols * Update for cliff commandmanager >=1.6.1 * Implement CRUD operations for Mapping objects * Update gitignore * Place the command to generate docs on one line * Remove duplicate env function in shell.py * Add functional tests to osc * Add low-level API base class * Test top-to-bottom: object-store containers * Updated from global requirements * utils.find\_resource does not catch right exception * Remove unused reference to keyring * Use oslo.utils * Updated from global requirements * Add service catalog commands * Add preliminary save container support * Add preliminary support for downloading objects * Updated from global requirements * Use Keystone client session.Session * Add action 'user password set' for identiy v3 * Unordered dicts and lists causes variable results * Leverage openstack.common.importutils for import\_class * Work toward Python 3.4 support and testing * Make Identity client load like the others * Change app.restapi to app.client\_manager.session * Add i18n module to openstackclient * Add oslo.i18n as a dependency * Updated from global requirements * Add commands for object upload and delete * Use oslosphinx to generate documentation * Updated from global requirements * Add container create and delete support * Fix PEP8 E302 errors * Add Python 3 support * Fix server resize * Add basic timing support * Move network stuff to v2 instead of v2\_0 * Catch SystemExit for parse args * Python 3: remove a useless code to safe\_encode() * Remove keyring support from openstackclient * trust authentication * Updated from global requirements * Sort/clean setup.cfg * Rename token classes to match command * Fix PEP8 E126 and E202 errors * Fix PEP8 E265 errors * Fix PEP8 H405 errors * Network CRUD * Updated from global requirements * Updated from global requirements * Change the token verb to issue/revoke * Add a docs job to tox.ini * Fix find\_resource for keystone and cinder * Refactor oauth1 code for updates * Updated from global requirements * Clean up logging levels * Ignore most of the new hacking 0.9.2 rules * Add support for extension list * Add role assignments list support to identity v3 * Add token delete command for identity v2 * Fixed several typos throughout the codebase * replace string format arguments with function parameters * Updated from global requirements * Implement CRUD operations for Identity Providers * Updated from global requirements * move read\_blob\_file\_contents to utils * Produce a useful error message for NoUniqueMatch * Updated from global requirements * Make bash comple command best effort to authorize * Updated from global requirements * In anticipation of network agents, rename compute * Updated from global requirements * Add ability to prompt for passwords for user create and set * Fix some help strings * Use six.iteritems() rather than dict.iteritems() * Remove tox locale overrides * Add token create subcommand for identity v3 api * Updated from global requirements * Remove copyright from empty files * Add token create subcommand for identity v2 api * Sync with global requirements * Add support for specifying custom domains * Displaying curl commands for nova and cinder calls * Remove mox3 requirement * Updated from global requirements * Add missing requests and six requirements * Add module list command * Update OSC's CommandManager subclass * Bring RESTApi closer to ithe imminent keystoneclient.Session * Add return Closes-Bug: 1246356 * Restore Object API name 'object-store' * Expand support for command extensions * Fix typo * Support building wheels (PEP-427) * Add server image create command * Complete basic test infrastructure * change execute to run * Update URL for global hacking doc and fix typos * Remove httpretty from test requirements * Updated from global requirements * Updated from global requirements * Add options to support TLS certificate verification * Updated from global requirements * Add object-store show commands * Sort entrypoints in setup.cfg * Fix security group entrypoints * Delay authentication to handle commands that do not require it * Prepare for Identity v3 tests * Add to clientmanager tests * Add Identity v2 role and service tests * Refactor fake data for projects and users * Update tox.ini for new tox 1.6 config * Update requirements.txt and test-requirements.txt * Object API commands using our REST API layer * Create a new base REST API interface * Re-order oauth commands and sync with keystoneclient * Add Identity v2 project tests * Updated from global requirements * Sync with global requirements * Change version reporting to use pbr * Prep for 0.2 release (0.2.rc1) * Remove 'oauth authorization show' function from identity v3 * Remove tenant round 3 - other commands * Remove tenant round 2 - Identity API * Remove tenant round 1 - global options * Add server ssh command * Add security group commands * Add server resize command * Add server migrate command * Add server commands: (un)lock, (un)rescue, (un)set, add/remove volume * Add usage command for compute api * Clean up properties (metadata) formatting * Add aggregate commands * Complete Image v1 * Add quota commands * Add list and delete authorizations for oauth commands * Add show limits command * Remove api = apiName calls from each method * Add authenticate method to oauth code * Add EC2 credentials CRUD * Finish up v3 role commands * Add methods for user and group interactions * Move tests into project package * Add OAuth support for Identity V3 * Remove explicit distribute depend * Add volume backup commands * python3: Introduce py33 to tox.ini * Rename requires files to standard names * Fix identity v2.0 entry point * Tweak volume commands and add k=v argparse action * Migrate to pbr * Migrate to flake8 * Fix flake8 errors in anticipation of flake8 patch * Switch to noun-verb command forms * Add console commands * Adds image \`create\` and \`delete\` functionality * Add fixed-ip and floating-ip commands * Add compute keypair commands * Add policy to identity v3 * Add metadata support for volume * Make entry point strings readable * Add extra-specs support for volume-type * Add endpoint v3 functionality * Add service v3 support for identity * Add functionality for add-role commands * Add a simple extension hook * Add role v3 support to identity in openstack client * Added compute hypervisor support * Turn down requests logging level * Add snapshot support for v1 volume * add domain, credential to identity v3 api * Add volume support for openstack client * Add compute hosts support * Add metadata support for volume type * Added compute service support * Add quota v1 support for volume * Added compute flavor support * Added compute agent support * Correct the version mapping to image service * Add Cinder API V1 Support * Multiple API version support * Update .coveragerc * Upgraded to PEP8 1.3.3 to stay aligned with Nova, etc * Clean up test\_shell so that the tests are captured though the test framework * Use install\_venv\_common.py from oslo * v3 identity - group and project api * Sync latest openstack-common updates * Standardize on a copyright header and ensure all files have them * Migrate from nose to testr * Clean up test environment and remove unused imports * Updated gitignore and manifest * Adds Glance API v2 support * Fixes setup compatibility issue on Windows * Add OpenStack trove classifier for PyPI * Update compute client bits * Keyring support for openstackclient * If no password in env or command line, try prompting * Add read\_versioninfo method * Add post-tag versioning * Fix pep8 issues * Move docs to doc * minor fixes * Add role CRUD commands * Add endpoint CRUD commands * Clean up tenant and server * Update service commands * Add user CRUD commands * fix authentication setup in interactive mode and improve error handling so tracebacks are not printed twice * Revise command boolean flags * Move get\_client\_class() to common.utils * Add tenant CRUD commands * Add API versioning support * look at the command the user is going to run before trying to authenticate them * Add copyright notices and update dates * Add tenant commands, work on service * More identity client config * Remove printt * Add Identity to ClientManager * Fix "help" command and implement "list server" and "show server" * Change binary name to 'openstack' * Auto generate AUTHORS for python-openstackclient * Shell init & logging * Reset project version to 0.1 * Add openstack-common and test infrastructure * Cleanup auth client path * Add 'list service' command and common modules * Add token auth to shell and README * Begin to add Keystone auth * Change to argparse to match cliff 0.2 * Use cliff * Set up common utils * Add openstackclient bits * First commit